ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç db/upgrade.php000064400000013037152265520010007273 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file keeps track of upgrades to the lti module * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * xmldb_lti_upgrade is the function that upgrades * the lti module database when is needed * * This function is automaticly called when version number in * version.php changes. * * @param int $oldversion New old version number. * * @return boolean */ function xmldb_lti_upgrade($oldversion) { global $CFG, $DB, $OUTPUT; $dbman = $DB->get_manager(); // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2023070501) { // Define table lti_types_categories to be created. $table = new xmldb_table('lti_types_categories'); // Adding fields to table lti_types_categories. $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('typeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); // Adding keys to table lti_types_categories. $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); $table->add_key('typeid', XMLDB_KEY_FOREIGN, ['typeid'], 'lti_types', ['id']); $table->add_key('categoryid', XMLDB_KEY_FOREIGN, ['categoryid'], 'course_categories', ['id']); // Conditionally launch create table for lti_types_categories. if (!$dbman->table_exists($table)) { $dbman->create_table($table); } // Lti savepoint reached. upgrade_mod_savepoint(true, 2023070501, 'lti'); } if ($oldversion < 2023081101) { // Define table to override coursevisible for a tool on course level. $table = new xmldb_table('lti_coursevisible'); // Adding fields to table lti_coursevisible. $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE); $table->add_field('typeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id'); $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'typeid'); $table->add_field('coursevisible', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null, 'courseid'); // Add key. $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); // Define index courseid (not unique) to be added to lti_coursevisible. $table->add_index('courseid', XMLDB_INDEX_NOTUNIQUE, ['courseid']); // Define index typeid (not unique) to be added to lti_coursevisible. $table->add_index('typeid', XMLDB_INDEX_NOTUNIQUE, ['typeid']); // Conditionally launch create table for overriding coursevisible. if (!$dbman->table_exists($table)) { $dbman->create_table($table); } // Lti savepoint reached. upgrade_mod_savepoint(true, 2023081101, 'lti'); } // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } db/install.xml000064400000034121152265520010007320 0ustar00
db/events.php000064400000002154152265520010007146 0ustar00. /** * LTI Auth plugin event handler definition. * * @package auth_lti * @category event * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $observers = [ [ 'eventname' => '\core\event\user_loggedin', 'callback' => '\auth_lti\local\ltiadvantage\event\event_handler::handle_user_loggedin', ], ]; lang/en/auth_lti.php000064400000006551152265520010010416 0ustar00. /** * Strings for component 'auth_lti', language 'en'. * * @package auth_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['accountcreatedsuccess'] = 'Your account has been created and is now ready to use.'; $string['accountlinkedsuccess'] = 'Your existing account has been successfully linked.'; $string['auth_ltidescription'] = 'The LTI authentication plugin, together with the \'Publish as LTI tool\' enrolment plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.'; $string['cannotcreateaccounts'] = 'Account creation is currently prohibited on this site.'; $string['createaccount'] = 'Create account'; $string['createaccountforme'] = 'Create an account for me'; $string['createnewaccount'] = 'I\'d like to create a new account'; $string['currentlyloggedinas'] = 'You are currently logged in as:'; $string['firstlaunchnotice'] = 'It looks like this is your first time here. Please select from one of the following account options.'; $string['getstartedwithnewaccount'] = 'Get started with a new account'; $string['haveexistingaccount'] = 'I have an existing account'; $string['linkthisaccount'] = 'Link this account'; $string['mustbeloggedin'] = 'Log in to link your existing account.'; $string['pluginname'] = 'LTI'; $string['privacy:metadata:auth_lti'] = 'LTI authentication'; $string['privacy:metadata:auth_lti:authsubsystem'] = 'This plugin is connected to the authentication subsystem.'; $string['privacy:metadata:auth_lti:issuer'] = 'The issuer URL identifying the platform to which the linked user belongs.'; $string['privacy:metadata:auth_lti:issuer256'] = 'The SHA256 hash of the issuer URL.'; $string['privacy:metadata:auth_lti:sub'] = 'The subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:sub256'] = 'The SHA256 hash of the subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:tableexplanation'] = 'LTI accounts linked to a user\'s Moodle account.'; $string['privacy:metadata:auth_lti:timecreated'] = 'The timestamp when the user account was linked to the LTI login.'; $string['privacy:metadata:auth_lti:timemodified'] = 'The timestamp when this record was modified.'; $string['privacy:metadata:auth_lti:userid'] = 'The ID of the user account which the LTI login is linked to'; $string['provisioningmodeauto'] = 'New accounts only (automatic)'; $string['provisioningmodenewexisting'] = 'Existing and new accounts (prompt)'; $string['provisioningmodeexistingonly'] = 'Existing accounts only (prompt)'; $string['useexistingaccount'] = 'Use existing account'; $string['welcome'] = 'Welcome!'; login.php000064400000012472152265520010006371 0ustar00. /** * LTI 1.3 login endpoint. * * See: http://www.imsglobal.org/spec/security/v1p0/#step-1-third-party-initiated-login * * This must support both POST and GET methods, as per the spec. * * @package enrol_lti * @copyright 2021 Jake Dallimore out(false), // Resource link launches. (new moodle_url('/enrol/lti/launch_deeplink.php'))->out(false) // Deep linking launches. ]; // This code verifies the target_link_uri. Only two values are permitted (see endpoints listed above). if (!in_array($targetlinkuri, $validuris)) { $msg = 'The target_link_uri param must match one of the redirect URIs set during tool registration.'; throw new coding_exception($msg); } // Because client_id is optional, this endpoint receives a param 'id', a unique id generated when creating the registration. // A registration can thus be located by either the tuple {iss, client_id} (if client_id is provided), or by the tuple {iss, id}, // (if client_id is not provided). See https://www.imsglobal.org/spec/lti/v1p3/#client_id-login-parameter. global $_REQUEST; if (empty($_REQUEST['client_id']) && !empty($_REQUEST['id'])) { $_REQUEST['client_id'] = $_REQUEST['id']; } // Before beginning the OIDC authentication, ensure the MoodleSession cookie can be used. Browser-specific steps may need to be // taken to set cookies in 3rd party contexts. Skip the check if the user is already auth'd. This means that either cookies aren't // an issue in the current browser/launch context. if (!isloggedin()) { cookie_helper::do_cookie_check(new moodle_url('/enrol/lti/login.php', [ 'iss' => $iss, 'login_hint' => $loginhint, 'target_link_uri' => $targetlinkuri, 'lti_message_hint' => $ltimessagehint, 'client_id' => $_REQUEST['client_id'], ])); if (!cookie_helper::cookies_supported()) { global $OUTPUT, $PAGE; $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/enrol/lti/login.php')); $PAGE->set_pagelayout('popup'); echo $OUTPUT->header(); $renderer = $PAGE->get_renderer('enrol_lti'); echo $renderer->render_cookies_required_notice(); echo $OUTPUT->footer(); die(); } } // Now, do the OIDC login. $redirecturl = LtiOidcLogin::new( new issuer_database(new application_registration_repository(), new deployment_repository()), new launch_cache_session(), new lti_cookie() )->getRedirectUrl($targetlinkuri, $_REQUEST); redirect($redirecturl); lib.php000064400000063165152265520010006034 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains a library of functions and constants for the lti module * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once(__DIR__ . '/deprecatedlib.php'); /** * List of features supported in URL module * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function lti_supports($feature) { switch ($feature) { case FEATURE_GROUPS: case FEATURE_GROUPINGS: return false; case FEATURE_MOD_INTRO: case FEATURE_COMPLETION_TRACKS_VIEWS: case FEATURE_GRADE_HAS_GRADE: case FEATURE_GRADE_OUTCOMES: case FEATURE_BACKUP_MOODLE2: case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_OTHER; default: return null; } } /** * Given an object containing all the necessary data, * (defined by the form in mod.html) this function * will create a new instance and return the id number * of the new instance. * * @param object $instance An object from the form in mod.html * @return int The id of the newly inserted basiclti record **/ function lti_add_instance($lti, $mform) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); if (!isset($lti->toolurl)) { $lti->toolurl = ''; } lti_load_tool_if_cartridge($lti); $lti->timecreated = time(); $lti->timemodified = $lti->timecreated; $lti->servicesalt = uniqid('', true); if (!isset($lti->typeid)) { $lti->typeid = null; } lti_force_type_config_settings($lti, lti_get_type_config_by_instance($lti)); if (empty($lti->typeid) && isset($lti->urlmatchedtypeid)) { $lti->typeid = $lti->urlmatchedtypeid; } if (!isset($lti->instructorchoiceacceptgrades) || $lti->instructorchoiceacceptgrades != LTI_SETTING_ALWAYS) { // The instance does not accept grades back from the provider, so set to "No grade" value 0. $lti->grade = 0; } $lti->id = $DB->insert_record('lti', $lti); if (isset($lti->instructorchoiceacceptgrades) && $lti->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS) { if (!isset($lti->cmidnumber)) { $lti->cmidnumber = ''; } lti_grade_item_update($lti); } $services = lti_get_services(); foreach ($services as $service) { $service->instance_added( $lti ); } $completiontimeexpected = !empty($lti->completionexpected) ? $lti->completionexpected : null; \core_completion\api::update_completion_date_event($lti->coursemodule, 'lti', $lti->id, $completiontimeexpected); return $lti->id; } /** * Given an object containing all the necessary data, * (defined by the form in mod.html) this function * will update an existing instance with new data. * * @param object $instance An object from the form in mod.html * @return boolean Success/Fail **/ function lti_update_instance($lti, $mform) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); lti_load_tool_if_cartridge($lti); $lti->timemodified = time(); $lti->id = $lti->instance; if (!isset($lti->showtitlelaunch)) { $lti->showtitlelaunch = 0; } if (!isset($lti->showdescriptionlaunch)) { $lti->showdescriptionlaunch = 0; } lti_force_type_config_settings($lti, lti_get_type_config_by_instance($lti)); if (isset($lti->instructorchoiceacceptgrades) && $lti->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS) { lti_grade_item_update($lti); } else { // Instance is no longer accepting grades from Provider, set grade to "No grade" value 0. $lti->grade = 0; $lti->instructorchoiceacceptgrades = 0; lti_grade_item_delete($lti); } if ($lti->typeid == 0 && isset($lti->urlmatchedtypeid)) { $lti->typeid = $lti->urlmatchedtypeid; } $services = lti_get_services(); foreach ($services as $service) { $service->instance_updated( $lti ); } $completiontimeexpected = !empty($lti->completionexpected) ? $lti->completionexpected : null; \core_completion\api::update_completion_date_event($lti->coursemodule, 'lti', $lti->id, $completiontimeexpected); return $DB->update_record('lti', $lti); } /** * Given an ID of an instance of this module, * this function will permanently delete the instance * and any data that depends on it. * * @param int $id Id of the module instance * @return boolean Success/Failure **/ function lti_delete_instance($id) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); if (! $basiclti = $DB->get_record("lti", array("id" => $id))) { return false; } $result = true; // Delete any dependent records here. lti_grade_item_delete($basiclti); $ltitype = $DB->get_record('lti_types', array('id' => $basiclti->typeid)); if ($ltitype) { $DB->delete_records('lti_tool_settings', array('toolproxyid' => $ltitype->toolproxyid, 'course' => $basiclti->course, 'coursemoduleid' => $id)); } $cm = get_coursemodule_from_instance('lti', $id); \core_completion\api::update_completion_date_event($cm->id, 'lti', $id, null); // We must delete the module record after we delete the grade item. if ($DB->delete_records("lti", array("id" => $basiclti->id)) ) { $services = lti_get_services(); foreach ($services as $service) { $service->instance_deleted( $id ); } return true; } return false; } /** * Return the preconfigured tools which are configured for inclusion in the activity picker. * * @param \core_course\local\entity\content_item $defaultmodulecontentitem reference to the content item for the LTI module. * @param \stdClass $user the user object, to use for cap checks if desired. * @param stdClass $course the course to scope items to. * @return array the array of content items. */ function lti_get_course_content_items(\core_course\local\entity\content_item $defaultmodulecontentitem, \stdClass $user, \stdClass $course) { global $CFG, $OUTPUT; require_once($CFG->dirroot.'/mod/lti/locallib.php'); $types = []; // Use of a tool type, whether site or course level, is controlled by the following cap. if (!has_capability('mod/lti:addpreconfiguredinstance', \core\context\course::instance($course->id), $user)) { return $types; } $preconfiguredtools = lti_get_configured_types($course->id, $defaultmodulecontentitem->get_link()->param('sr')); foreach ($preconfiguredtools as $preconfiguredtool) { // Append the help link to the help text. if (isset($preconfiguredtool->help)) { if (isset($preconfiguredtool->helplink)) { $linktext = get_string('morehelp'); $preconfiguredtool->help .= html_writer::tag('div', $OUTPUT->doc_link($preconfiguredtool->helplink, $linktext, true), ['class' => 'helpdoclink']); } } else { $preconfiguredtool->help = ''; } // Preconfigured tools take their own id + 1. This logic exists because, previously, the entry permitting manual instance // creation (the $defaultmodulecontentitem, or 'External tool' item) was included and had the id 1. This logic prevented id // collisions. $types[] = new \core_course\local\entity\content_item( $preconfiguredtool->id + 1, $preconfiguredtool->name, new \core_course\local\entity\string_title($preconfiguredtool->title), $preconfiguredtool->link, $preconfiguredtool->icon, $preconfiguredtool->help, $defaultmodulecontentitem->get_archetype(), $defaultmodulecontentitem->get_component_name(), $defaultmodulecontentitem->get_purpose() ); } return $types; } /** * Return all content items which can be added to any course. * * @param \core_course\local\entity\content_item $defaultmodulecontentitem * @return array the array of content items. */ function mod_lti_get_all_content_items(\core_course\local\entity\content_item $defaultmodulecontentitem): array { global $OUTPUT, $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); // For access to constants. $types = []; foreach (lti_get_lti_types() as $ltitype) { if ($ltitype->coursevisible != LTI_COURSEVISIBLE_ACTIVITYCHOOSER) { continue; } $type = new stdClass(); $type->id = $ltitype->id; $type->modclass = MOD_CLASS_ACTIVITY; $type->name = 'lti_type_' . $ltitype->id; // Clean the name. We don't want tags here. $type->title = clean_param($ltitype->name, PARAM_NOTAGS); $trimmeddescription = trim($ltitype->description ?? ''); $type->help = ''; if ($trimmeddescription != '') { // Clean the description. We don't want tags here. $type->help = clean_param($trimmeddescription, PARAM_NOTAGS); $type->helplink = get_string('modulename_shortcut_link', 'lti'); } if (empty($ltitype->icon)) { $type->icon = $OUTPUT->pix_icon('monologo', '', 'lti', array('class' => 'icon')); } else { $type->icon = html_writer::empty_tag('img', array('src' => $ltitype->icon, 'alt' => $ltitype->name, 'class' => 'icon')); } $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'typeid' => $ltitype->id)); // Preconfigured tools take their own id + 1. This logic exists because, previously, the entry permitting manual instance // creation (the $defaultmodulecontentitem, or 'External tool' item) was included and had the id 1. This logic prevented id // collisions. $types[] = new \core_course\local\entity\content_item( $type->id + 1, $type->name, new \core_course\local\entity\string_title($type->title), $type->link, $type->icon, $type->help, $defaultmodulecontentitem->get_archetype(), $defaultmodulecontentitem->get_component_name(), $defaultmodulecontentitem->get_purpose() ); } return $types; } /** * Given a coursemodule object, this function returns the extra * information needed to print this activity in various places. * For this module we just need to support external urls as * activity icons * * @param stdClass $coursemodule * @return cached_cm_info info */ function lti_get_coursemodule_info($coursemodule) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); if (!$lti = $DB->get_record('lti', array('id' => $coursemodule->instance), 'icon, secureicon, intro, introformat, name, typeid, toolurl, launchcontainer')) { return null; } $info = new cached_cm_info(); if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $info->content = format_module_intro('lti', $lti, $coursemodule->id, false); } if (!empty($lti->typeid)) { $toolconfig = lti_get_type_config($lti->typeid); } else if ($tool = lti_get_tool_by_url_match($lti->toolurl)) { $toolconfig = lti_get_type_config($tool->id); } else { $toolconfig = array(); } // We want to use the right icon based on whether the // current page is being requested over http or https. if (lti_request_is_using_ssl() && (!empty($lti->secureicon) || (isset($toolconfig['secureicon']) && !empty($toolconfig['secureicon'])))) { if (!empty($lti->secureicon)) { $info->iconurl = new moodle_url($lti->secureicon); } else { $info->iconurl = new moodle_url($toolconfig['secureicon']); } } else if (!empty($lti->icon)) { $info->iconurl = new moodle_url($lti->icon); } else if (isset($toolconfig['icon']) && !empty($toolconfig['icon'])) { $info->iconurl = new moodle_url($toolconfig['icon']); } // Does the link open in a new window? $launchcontainer = lti_get_launch_container($lti, $toolconfig); if ($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) { $launchurl = new moodle_url('/mod/lti/launch.php', array('id' => $coursemodule->id)); $info->onclick = "window.open('" . $launchurl->out(false) . "', 'lti-".$coursemodule->id."'); return false;"; } $info->name = $lti->name; return $info; } /** * Return a small object with summary information about what a * user has done with a given particular instance of this module * Used for user activity reports. * $return->time = the time they did it * $return->info = a short text description * * @return null * @TODO: implement this moodle function (if needed) **/ function lti_user_outline($course, $user, $mod, $basiclti) { return null; } /** * Print a detailed representation of what a user has done with * a given particular instance of this module, for user activity reports. * * @return boolean * @TODO: implement this moodle function (if needed) **/ function lti_user_complete($course, $user, $mod, $basiclti) { return true; } /** * Given a course and a time, this module should find recent activity * that has occurred in basiclti activities and print it out. * Return true if there was output, or false is there was none. * * @uses $CFG * @return boolean * @TODO: implement this moodle function **/ function lti_print_recent_activity($course, $isteacher, $timestart) { return false; // True if anything was printed, otherwise false. } /** * Function to be run periodically according to the moodle cron * This function searches for things that need to be done, such * as sending out mail, toggling flags etc ... * * @uses $CFG * @return boolean **/ function lti_cron () { return true; } /** * Must return an array of grades for a given instance of this module, * indexed by user. It also returns a maximum allowed grade. * * Example: * $return->grades = array of grades; * $return->maxgrade = maximum allowed grade; * * return $return; * * @param int $basicltiid ID of an instance of this module * @return mixed Null or object with an array of grades and with the maximum grade * * @TODO: implement this moodle function (if needed) **/ function lti_grades($basicltiid) { return null; } /** * Checks if scale is being used by any instance of basiclti. * This function was added in 1.9 * * This is used to find out if scale used anywhere * @param $scaleid int * @return boolean True if the scale is used by any basiclti * */ function lti_scale_used_anywhere($scaleid) { global $DB; if ($scaleid and $DB->record_exists('lti', array('grade' => -$scaleid))) { return true; } else { return false; } } /** * Execute post-install custom actions for the module * This function was added in 1.9 * * @return boolean true if success, false on error */ function lti_install() { return true; } /** * Execute post-uninstall custom actions for the module * This function was added in 1.9 * * @return boolean true if success, false on error */ function lti_uninstall() { return true; } /** * Returns available Basic LTI types * * @return array of basicLTI types */ function lti_get_lti_types() { global $DB; return $DB->get_records('lti_types', null, 'state DESC, timemodified DESC'); } /** * Returns available Basic LTI types that match the given * tool proxy id * * @param int $toolproxyid Tool proxy id * @return array of basicLTI types */ function lti_get_lti_types_from_proxy_id($toolproxyid) { global $DB; return $DB->get_records('lti_types', array('toolproxyid' => $toolproxyid), 'state DESC, timemodified DESC'); } /** * Create grade item for given basiclti * * @category grade * @param object $basiclti object with extra cmidnumber * @param mixed optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ function lti_grade_item_update($basiclti, $grades = null) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/mod/lti/servicelib.php'); if (!lti_accepts_grades($basiclti)) { return 0; } $params = array('itemname' => $basiclti->name, 'idnumber' => $basiclti->cmidnumber); if ($basiclti->grade > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $basiclti->grade; $params['grademin'] = 0; } else if ($basiclti->grade < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; $params['scaleid'] = -$basiclti->grade; } else { $params['gradetype'] = GRADE_TYPE_TEXT; // Allow text comments only. } if ($grades === 'reset') { $params['reset'] = true; $grades = null; } return grade_update('mod/lti', $basiclti->course, 'mod', 'lti', $basiclti->id, 0, $grades, $params); } /** * Update activity grades * * @param stdClass $basiclti The LTI instance * @param int $userid Specific user only, 0 means all. * @param bool $nullifnone Not used */ function lti_update_grades($basiclti, $userid=0, $nullifnone=true) { global $CFG; require_once($CFG->dirroot.'/mod/lti/servicelib.php'); // LTI doesn't have its own grade table so the only thing to do is update the grade item. if (lti_accepts_grades($basiclti)) { lti_grade_item_update($basiclti); } } /** * Delete grade item for given basiclti * * @category grade * @param object $basiclti object * @return object basiclti */ function lti_grade_item_delete($basiclti) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update('mod/lti', $basiclti->course, 'mod', 'lti', $basiclti->id, 0, null, array('deleted' => 1)); } /** * Log post actions * * @return array */ function lti_get_post_actions() { return array(); } /** * Log view actions * * @return array */ function lti_get_view_actions() { return array('view all', 'view'); } /** * Mark the activity completed (if required) and trigger the course_module_viewed event. * * @param stdClass $lti lti object * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @since Moodle 3.0 */ function lti_view($lti, $course, $cm, $context) { // Trigger course_module_viewed event. $params = array( 'context' => $context, 'objectid' => $lti->id ); $event = \mod_lti\event\course_module_viewed::create($params); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('lti', $lti); $event->trigger(); // Completion. $completion = new completion_info($course); $completion->set_module_viewed($cm); } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function lti_check_updates_since(cm_info $cm, $from, $filter = array()) { global $DB, $USER; $updates = course_check_module_updates_since($cm, $from, array(), $filter); // Check if there is a new submission. $updates->submissions = (object) array('updated' => false); $select = 'ltiid = :id AND userid = :userid AND (datesubmitted > :since1 OR dateupdated > :since2)'; $params = array('id' => $cm->instance, 'userid' => $USER->id, 'since1' => $from, 'since2' => $from); $submissions = $DB->get_records_select('lti_submission', $select, $params, '', 'id'); if (!empty($submissions)) { $updates->submissions->updated = true; $updates->submissions->itemids = array_keys($submissions); } // Now, teachers should see other students updates. if (has_capability('mod/lti:manage', $cm->context)) { $select = 'ltiid = :id AND (datesubmitted > :since1 OR dateupdated > :since2)'; $params = array('id' => $cm->instance, 'since1' => $from, 'since2' => $from); if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); if (empty($groupusers)) { return $updates; } list($insql, $inparams) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED); $select .= ' AND userid ' . $insql; $params = array_merge($params, $inparams); } $updates->usersubmissions = (object) array('updated' => false); $submissions = $DB->get_records_select('lti_submission', $select, $params, '', 'id'); if (!empty($submissions)) { $updates->usersubmissions->updated = true; $updates->usersubmissions->itemids = array_keys($submissions); } } return $updates; } /** * Get icon mapping for font-awesome. */ function mod_lti_get_fontawesome_icon_map() { return [ 'mod_lti:warning' => 'fa-exclamation text-warning', ]; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_lti_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, int $userid = 0) { global $USER; if (empty($userid)) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['lti'][$event->instance]; if (!$cm->uservisible) { // The module is not visible to the user for any reason. return null; } $completion = new \completion_info($cm->get_course()); $completiondata = $completion->get_data($cm, false, $userid); if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { return null; } return $factory->create_instance( get_string('view'), new \moodle_url('/mod/lti/view.php', ['id' => $cm->id]), 1, true ); } /** * Extend the course navigation with an "LTI External tools" link which redirects to a list of all tools available for course use. * * @param settings_navigation $navigation The settings navigation object * @param stdClass $course The course * @param stdclass $context Course context * @return void */ function mod_lti_extend_navigation_course($navigation, $course, $context): void { if (has_capability('mod/lti:addpreconfiguredinstance', $context)) { $url = new moodle_url('/mod/lti/coursetools.php', ['id' => $course->id]); $settingsnode = navigation_node::create(get_string('courseexternaltools', 'mod_lti'), $url, navigation_node::TYPE_SETTING, null, 'coursetools', new pix_icon('i/settings', '')); $navigation->add_node($settingsnode); } } version.php000064400000005120152265520010006736 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines the version of lti * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'mod_lti'; // Full name of the plugin (used for diagnostics). $plugin->cron = 0; classes/privacy/provider.php000064400000043024152265520010012222 0ustar00. /** * Privacy Subsystem implementation for mod_lti. * * @package mod_lti * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem implementation for mod_lti. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Return the fields which contain personal data. * * @param collection $items a reference to the collection to use to store the metadata. * @return collection the updated collection of metadata items. */ public static function get_metadata(collection $items): collection { $items->add_external_location_link( 'lti_provider', [ 'userid' => 'privacy:metadata:userid', 'username' => 'privacy:metadata:username', 'useridnumber' => 'privacy:metadata:useridnumber', 'firstname' => 'privacy:metadata:firstname', 'lastname' => 'privacy:metadata:lastname', 'fullname' => 'privacy:metadata:fullname', 'email' => 'privacy:metadata:email', 'role' => 'privacy:metadata:role', 'courseid' => 'privacy:metadata:courseid', 'courseidnumber' => 'privacy:metadata:courseidnumber', 'courseshortname' => 'privacy:metadata:courseshortname', 'coursefullname' => 'privacy:metadata:coursefullname', ], 'privacy:metadata:externalpurpose' ); $items->add_database_table( 'lti_submission', [ 'userid' => 'privacy:metadata:lti_submission:userid', 'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted', 'dateupdated' => 'privacy:metadata:lti_submission:dateupdated', 'gradepercent' => 'privacy:metadata:lti_submission:gradepercent', 'originalgrade' => 'privacy:metadata:lti_submission:originalgrade', ], 'privacy:metadata:lti_submission' ); $items->add_database_table( 'lti_tool_proxies', [ 'name' => 'privacy:metadata:lti_tool_proxies:name', 'createdby' => 'privacy:metadata:createdby', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified' ], 'privacy:metadata:lti_tool_proxies' ); $items->add_database_table( 'lti_types', [ 'name' => 'privacy:metadata:lti_types:name', 'createdby' => 'privacy:metadata:createdby', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified' ], 'privacy:metadata:lti_types' ); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid the userid. * @return contextlist the list of contexts containing user info for the user. */ public static function get_contexts_for_userid(int $userid): contextlist { // Fetch all LTI submissions. $sql = "SELECT c.id FROM {context} c INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname INNER JOIN {lti} lti ON lti.id = cm.instance INNER JOIN {lti_submission} ltisub ON ltisub.ltiid = lti.id WHERE ltisub.userid = :userid"; $params = [ 'modname' => 'lti', 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); // Fetch all LTI types. $sql = "SELECT c.id FROM {context} c JOIN {course} course ON c.contextlevel = :contextlevel AND c.instanceid = course.id JOIN {lti_types} ltit ON ltit.course = course.id WHERE ltit.createdby = :userid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'userid' => $userid ]; $contextlist->add_from_sql($sql, $params); // The LTI tool proxies sit in the system context. $contextlist->add_system_context(); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } // Fetch all LTI submissions. $sql = "SELECT ltisub.userid FROM {context} c INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname INNER JOIN {lti} lti ON lti.id = cm.instance INNER JOIN {lti_submission} ltisub ON ltisub.ltiid = lti.id WHERE c.id = :contextid"; $params = [ 'modname' => 'lti', 'contextlevel' => CONTEXT_MODULE, 'contextid' => $context->id, ]; $userlist->add_from_sql('userid', $sql, $params); // Fetch all LTI types. $sql = "SELECT ltit.createdby AS userid FROM {context} c JOIN {course} course ON c.contextlevel = :contextlevel AND c.instanceid = course.id JOIN {lti_types} ltit ON ltit.course = course.id WHERE c.id = :contextid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'contextid' => $context->id, ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ public static function export_user_data(approved_contextlist $contextlist) { self::export_user_data_lti_submissions($contextlist); self::export_user_data_lti_types($contextlist); self::export_user_data_lti_tool_proxies($contextlist); } /** * Delete all data for all users in the specified context. * * @param \context $context the context to delete in. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if (!$context instanceof \context_module) { return; } if ($cm = get_coursemodule_from_id('lti', $context->instanceid)) { $DB->delete_records('lti_submission', ['ltiid' => $cm->instance]); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist a list of contexts approved for deletion. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if (!$context instanceof \context_module) { continue; } $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); if ($context instanceof \context_module) { $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); list($insql, $inparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $sql = "ltiid = :instanceid AND userid {$insql}"; $params = array_merge(['instanceid' => $instanceid], $inparams); $DB->delete_records_select('lti_submission', $sql, $params); } } /** * Export personal data for the given approved_contextlist related to LTI submissions. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) { global $DB; // Filter out any contexts that are not related to modules. $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { if ($context->contextlevel == CONTEXT_MODULE) { $carry[] = $context->instanceid; } return $carry; }, []); if (empty($cmids)) { return; } $user = $contextlist->get_user(); // Get all the LTI activities associated with the above course modules. $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids); $ltiids = array_keys($ltiidstocmids); list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED); $params = array_merge($inparams, ['userid' => $user->id]); $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id'); self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) { $carry[] = [ 'gradepercent' => $record->gradepercent, 'originalgrade' => $record->originalgrade, 'datesubmitted' => transform::datetime($record->datesubmitted), 'dateupdated' => transform::datetime($record->dateupdated) ]; return $carry; }, function($ltiid, $data) use ($user, $ltiidstocmids) { $context = \context_module::instance($ltiidstocmids[$ltiid]); $contextdata = helper::get_context_data($context, $user); $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]); helper::export_context_files($context, $user); writer::with_context($context)->export_data([], $finaldata); }); } /** * Export personal data for the given approved_contextlist related to LTI types. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ protected static function export_user_data_lti_types(approved_contextlist $contextlist) { global $DB; // Filter out any contexts that are not related to courses. $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) { if ($context->contextlevel == CONTEXT_COURSE) { $carry[] = $context->instanceid; } return $carry; }, []); if (empty($courseids)) { return; } $user = $contextlist->get_user(); list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $params = array_merge($inparams, ['userid' => $user->id]); $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC'); self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) { $context = \context_course::instance($record->course); $options = ['context' => $context]; $carry[] = [ 'name' => format_string($record->name, true, $options), 'createdby' => transform::user($record->createdby), 'timecreated' => transform::datetime($record->timecreated), 'timemodified' => transform::datetime($record->timemodified) ]; return $carry; }, function($courseid, $data) { $context = \context_course::instance($courseid); $finaldata = (object) ['lti_types' => $data]; writer::with_context($context)->export_data([], $finaldata); }); } /** * Export personal data for the given approved_contextlist related to LTI tool proxies. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) { global $DB; // Filter out any contexts that are not related to system context. $systemcontexts = array_filter($contextlist->get_contexts(), function($context) { return $context->contextlevel == CONTEXT_SYSTEM; }); if (empty($systemcontexts)) { return; } $user = $contextlist->get_user(); $systemcontext = \context_system::instance(); $data = []; $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC'); foreach ($ltiproxies as $ltiproxy) { $data[] = [ 'name' => format_string($ltiproxy->name, true, ['context' => $systemcontext]), 'createdby' => transform::user($ltiproxy->createdby), 'timecreated' => transform::datetime($ltiproxy->timecreated), 'timemodified' => transform::datetime($ltiproxy->timemodified) ]; } $ltiproxies->close(); $finaldata = (object) ['lti_tool_proxies' => $data]; writer::with_context($systemcontext)->export_data([], $finaldata); } /** * Return a dict of LTI IDs mapped to their course module ID. * * @param array $cmids The course module IDs. * @return array In the form of [$ltiid => $cmid]. */ protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) { global $DB; list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); $sql = "SELECT lti.id, cm.id AS cmid FROM {lti} lti JOIN {modules} m ON m.name = :lti JOIN {course_modules} cm ON cm.instance = lti.id AND cm.module = m.id WHERE cm.id $insql"; $params = array_merge($inparams, ['lti' => 'lti']); return $DB->get_records_sql_menu($sql, $params); } /** * Loop and export from a recordset. * * @param \moodle_recordset $recordset The recordset. * @param string $splitkey The record key to determine when to export. * @param mixed $initial The initial data to reduce from. * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. * @return void */ protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, callable $reducer, callable $export) { $data = $initial; $lastid = null; foreach ($recordset as $record) { if ($lastid && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if (!empty($lastid)) { $export($lastid, $data); } } } classes/local/ltiadvantage/entity/user_migration_claim.php000064400000014744152265520010020167 0ustar00. namespace auth_lti\local\ltiadvantage\entity; /** * A simplified representation of a 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' migration claim. * * This serves the purpose of migrating a legacy user account only. Claim properties that do not relate to user migration are not * included or handled by this representation. * * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_migration_claim { /** @var string the LTI 1.1 consumer key */ private $consumerkey; /** @var string the LTI 1.1 user identifier. * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim. * If not included, the value will be taken from 'sub'. */ private $userid; /** * The migration_claim constructor. * * The signature of a migration claim must be verifiable. To achieve this, the constructor takes a list of secrets * corresponding to the 'oauth_consumer_key' provided in the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' * claim. How these secrets are determined is not the responsibility of this class. The constructor assumes these * correspond. * * @param array $jwt the array of claim data, as received in a resource link launch JWT. * @param array $consumersecrets a list of consumer secrets for the consumerkey included in the migration claim. * @throws \coding_exception if the claim data is invalid. */ public function __construct(array $jwt, array $consumersecrets) { // Can't get a claim instance without the claim data. if (empty($jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'])) { throw new \coding_exception("Missing the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' JWT claim"); } $claim = $jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1']; // The oauth_consumer_key property MUST be sent. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key. if (empty($claim['oauth_consumer_key'])) { throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim."); } // The oauth_consumer_key_sign property MAY be sent. // For user migration to take place, however, this is deemed a required property since Moodle identified its // legacy users through a combination of consumerkey and userid. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign. if (empty($claim['oauth_consumer_key_sign'])) { throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."); } if (!$this->verify_signature( $claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $jwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $jwt['iss'], $jwt['aud'], $jwt['exp'], $jwt['nonce'], $consumersecrets )) { throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim."); } $this->consumerkey = $claim['oauth_consumer_key']; $this->userid = $claim['user_id'] ?? $jwt['sub']; } /** * Verify the claim signature by recalculating it using the launch data and cross-checking consumer secrets. * * @param string $consumerkey the LTI 1.1 consumer key. * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data. * @param string $deploymentid the deployment id included in the launch. * @param string $platform the platform included in the launch. * @param string $clientid the client id included in the launch. * @param string $exp the exp included in the launch. * @param string $nonce the nonce included in the launch. * @param array $consumersecrets the list of consumer secrets used with the given $consumerkey param * @return bool true if the signature was verified, false otherwise. */ private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce, array $consumersecrets): bool { $base = [ $consumerkey, $deploymentid, $platform, $clientid, $exp, $nonce ]; $basestring = implode('&', $base); // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found // or no signatures match. foreach ($consumersecrets as $consumersecret) { $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret)); if ($signature === $calculatedsignature) { return true; } } return false; } /** * Return the consumer key stored in the claim. * * @return string the consumer key included in the claim. */ public function get_consumer_key(): string { return $this->consumerkey; } /** * Return the LTI 1.1 user id stored in the claim. * * @return string the user id, or null if not provided in the claim. */ public function get_user_id(): string { return $this->userid; } } classes/local/ltiadvantage/event/event_handler.php000064400000003764152265520010016416 0ustar00. namespace auth_lti\local\ltiadvantage\event; use auth_lti\local\ltiadvantage\utility\cookie_helper; use core\event\user_loggedin; /** * Event handler for auth_lti. * * @package auth_lti * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_handler { /** * Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls. * * @param user_loggedin $event the event * @return void */ public static function handle_user_loggedin(user_loggedin $event): void { // The event data isn't important here. The intent of this listener is to ensure that the MoodleSession cookie is set up // properly during LTI launches + login. This means two things: // i) it's set with SameSite=None; Secure; where possible (since OIDC needs HTTPS this will almost always be possible). // ii) it set with the 'Partitioned' attribute, when required. // The former ensures cross-site cookies are sent for embedded launches. The latter is an opt-in flag needed to use Chrome's // partitioning mechanism, CHIPS. if (cookie_helper::cookies_supported()) { cookie_helper::setup_session_cookie(); } } } classes/local/ltiadvantage/utility/cookie_helper.php000064400000020166152265520010016765 0ustar00. namespace auth_lti\local\ltiadvantage\utility; use core\session\utility\cookie_helper as core_cookie_helper; /** * Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies. * * @package auth_lti * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cookie_helper { /** @var int Cookies are not supported. */ public const COOKIE_METHOD_NOT_SUPPORTED = 0; /** @var int Cookies are supported without explicit partitioning. */ public const COOKIE_METHOD_NO_PARTITIONING = 1; /** @var int Cookies are supported via explicit partitioning. */ public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2; /** * Check whether cookies can be used with the current user agent and, if so, via what method they are set. * * Currently, this tries 2 modes of setting a test cookie: * 1. Setting a SameSite=None, Secure cookie. This will work in any first party context, and in 3rd party contexts for * any browsers supporting automatic partitioning of 3rd party cookies (E.g. Firefox, Brave). * 2. If 1 fails, setting a cookie with the Chrome 'Partitioned' attribute included, opting that cookie into CHIPS. This will * work for Chrome. * * Upon completion of the cookie check, the check sets a SESSION flag indicating the method used to set the cookie, and upgrades * the session cookie ('MoodleSession') using the respective method. This ensure the session cookie will continue to be sent. * * Then, the following methods can be used by client code to query whether the UA supports cookies, and how: * @see self::cookies_supported() - whether it could be set at all. * @see self::get_cookies_supported_mode() - if a cookie could be set, what mode was used to set it. * * This permits client code to make sure it's setting its cookies appropriately (via the advertised method), and allows it to * present notices - such as in the case where a given UA is found to be lacking the requisite cookie support. * E.g. * cookie_helper::do_cookie_check($mypageurl); * if (!cookie_helper::cookies_supported()) { * // Print a notice stating that cookie support is required. * } * // Elsewhere in other client code... * if (cookie_helper::get_cookies_supported_mode() === cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) { * // Set a cookie, making sure to use the helper to also opt-in to partitioning. * setcookie('myauthcookie', 'myauthcookievalue', ['samesite' => 'None', 'secure' => true]); * cookie_helper::add_partitioning_to_cookie('myauthcookie'); * } * * @param \moodle_url $pageurl the URL of the page making the check, used to redirect back to after setting test cookies. * @return void */ public static function do_cookie_check(\moodle_url $pageurl): void { global $_COOKIE, $SESSION, $CFG; $cookiecheck1 = optional_param('cookiecheck1', null, PARAM_INT); $cookiecheck2 = optional_param('cookiecheck2', null, PARAM_INT); if (empty($cookiecheck1)) { // Start the cookie check. Set two test cookies - one samesite none, and one partitioned - and redirect. // Set cookiecheck to show the check has started. self::set_test_cookie('cookiecheck1', self::COOKIE_METHOD_NO_PARTITIONING); self::set_test_cookie('cookiecheck2', self::COOKIE_METHOD_EXPLICIT_PARTITIONING, true); $pageurl->params([ 'cookiecheck1' => self::COOKIE_METHOD_NO_PARTITIONING, 'cookiecheck2' => self::COOKIE_METHOD_EXPLICIT_PARTITIONING, ]); // LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the // MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To // guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie. core_cookie_helper::expire_moodlesession(); redirect($pageurl); } else { // Have already started a cookie check, so check the result. $cookie1received = isset($_COOKIE['cookiecheck1']) && $_COOKIE['cookiecheck1'] == $cookiecheck1; $cookie2received = isset($_COOKIE['cookiecheck2']) && $_COOKIE['cookiecheck2'] == $cookiecheck2; if ($cookie1received || $cookie2received) { // The test cookie could be set and received. // Set a session flag storing the method used to set it, and make sure the session cookie uses this method. $cookiemethod = $cookie1received ? self::COOKIE_METHOD_NO_PARTITIONING : self::COOKIE_METHOD_EXPLICIT_PARTITIONING; $SESSION->auth_lti_cookie_method = $cookiemethod; self::setup_session_cookie(); } } } /** * If a cookie check has been made, returns whether cookies could be set or not. * * @return bool whether cookies are supported or not. */ public static function cookies_supported(): bool { return self::get_cookies_supported_method() !== self::COOKIE_METHOD_NOT_SUPPORTED; } /** * If a cookie check has been made, gets the method used to set a cookie, or self::COOKIE_METHOD_NOT_SUPPORTED if not supported. * * For cookie methods: * @see self::COOKIE_METHOD_NOT_SUPPORTED * @see self::COOKIE_METHOD_NO_PARTITIONING * @see self::COOKIE_METHOD_EXPLICIT_PARTITIONING * * @return int the constant representing the method by which the cookie was set, or not. */ public static function get_cookies_supported_method(): int { global $SESSION; return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED; } /** * Sets up the session cookie according to the method used in the cookie check, and with SameSite=None; Secure attributes. * * @return void */ public static function setup_session_cookie(): void { global $CFG; require_once($CFG->libdir . '/sessionlib.php'); if (is_moodle_cookie_secure()) { $atts = ['SameSite=None', 'Secure']; if (self::get_cookies_supported_method() == self::COOKIE_METHOD_EXPLICIT_PARTITIONING) { $atts[] = 'Partitioned'; } core_cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts); } } /** * Set a test cookie, using SameSite=None; Secure; attributes if possible, and with or without partitioning opt-in. * * @param string $name cookie name * @param string $value cookie value * @param bool $partitioned whether to try to add partitioning opt-in, which requires secure cookies (https sites). * @return void */ private static function set_test_cookie(string $name, string $value, bool $partitioned = false): void { global $CFG; require_once($CFG->libdir . '/sessionlib.php'); $atts = ['expires' => time() + 30]; if (is_moodle_cookie_secure()) { $atts['samesite'] = 'none'; $atts['secure'] = true; } setcookie($name, $value, $atts); if (is_moodle_cookie_secure() && $partitioned) { core_cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']); } } } classes/output/renderer.php000064400000006471152265520010012066 0ustar00. /** * Renderer class for template library. * * @package mod_lti * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; use plugin_renderer_base; /** * Renderer class for template library. * * @package mod_lti * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Defer to template. * * @param tool_configure_page $page * * @return string html for the page */ public function render_tool_configure_page($page) { $data = $page->export_for_template($this); return parent::render_from_template('mod_lti/tool_configure', $data); } /** * Render the external registration return page * * @param tool_configure_page $page * * @return string html for the page */ public function render_external_registration_return_page($page) { $data = $page->export_for_template($this); return parent::render_from_template('mod_lti/external_registration_return', $data); } /** * Render the external registration return page * * @param tool_configure_page $page * * @return string html for the page */ public function render_registration_upgrade_choice_page($page) { $data = $page->export_for_template($this); return parent::render_from_template('mod_lti/registration_upgrade_choice_page', $data); } /** * Render the reposting of the cross site request. * * @param repost_crosssite_page $page the page renderable. * * @return string rendered html for the page. */ public function render_repost_crosssite_page(repost_crosssite_page $page): string { $data = $page->export_for_template($this); return parent::render_from_template('mod_lti/repost_crosssite', $data); } /** * Render the course tools page header. * * @param course_tools_page $page the page renderable. * @return string the rendered html for the page. */ protected function render_course_tools_page(course_tools_page $page): string { // Render the table header templatable + the report. $headerrenderable = $page->get_header(); $table = $page->get_table(); $headercontext = $headerrenderable->export_for_template($this); $headeroutput = parent::render_from_template('mod_lti/course_tools_page_header', $headercontext); return $headeroutput . $table->output(); } } templates/local/ltiadvantage/login.mustache000064400000012303152265520010015137 0ustar00{{! 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 . }} {{! @template auth_lti/local/ltiadvantage/login Template which displays a choice screen for instructors on first launch, allowing them to select whether to use an existing account in the tool, or to auto provision a new one. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * formaction * sesskey * info - a notification describing the first launch options * cancreateaccounts - whether or not the user is allowed to create auth_lti accounts * accountinfo - information about the user, importantly whether they are logged in or not. Example context (json): { "formaction": "auth/lti/login.php", "sesskey": "1a2b3c4dfg", "info": { "message": "Looks like this is your first time here...", "extraclasses": "", "announce": false, "closebutton": false, "issuccess": true }, "cancreateaccounts": true, "isloggedin": true, "accountinfo": { "firstname": "John", "lastname": "Smith", "email": "john@example.com", "picturehtml": "\"\"" } } }}

{{#str}} welcome, auth_lti {{/str}}

{{#info}} {{> core/notification}} {{/info}}
{{#str}} haveexistingaccount, auth_lti {{/str}}

{{#str}} useexistingaccount, auth_lti {{/str}}

{{#isloggedin}} {{#accountinfo}}

{{#str}} currentlyloggedinas, auth_lti {{/str}}
{{{picturehtml}}} {{firstname}} {{lastname}} ({{email}})

{{/accountinfo}} {{/isloggedin}} {{^isloggedin}}

{{#str}} mustbeloggedin, auth_lti {{/str}}

{{/isloggedin}}
{{#cancreateaccounts}}
{{#str}} createnewaccount, auth_lti {{/str}}

{{#str}} createaccount, auth_lti {{/str}}

{{#str}} getstartedwithnewaccount, auth_lti {{/str}}

{{/cancreateaccounts}}
templates/local/ltiadvantage/account_binding_complete.mustache000064400000003357152265520010021056 0ustar00{{! 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 . }} {{! @template auth_lti/local/ltiadvantage/account_binding_complete Template which displays the confirmation after the user has either signed in and has their account linked, or has had an account automatically provisioned and linked. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * notification * returnurl Example context (json): { "notification": { "message": "Your account was successfully linked!", "extraclasses": "", "announce": true, "closebutton": false, "issuccess": true, "isinfo": false, "iswarning": false, "iserror": false }, "returnurl": "https://your.site/enrol/lti/launch_deeplink.php?id=123abc" } }}
{{#notification}} {{> core/notification}} {{/notification}} {{#str}}continue, core{{/str}}
tests/privacy/provider_test.php000064400000036463152265520010012777 0ustar00. /** * Privacy provider tests. * * @package mod_lti * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use mod_lti\privacy\provider; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * Privacy provider tests class. * * @package mod_lti * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Test for provider::get_metadata(). */ public function test_get_metadata(): void { $collection = new collection('mod_lti'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); $this->assertCount(4, $itemcollection); $ltiproviderexternal = array_shift($itemcollection); $this->assertEquals('lti_provider', $ltiproviderexternal->get_name()); $ltisubmissiontable = array_shift($itemcollection); $this->assertEquals('lti_submission', $ltisubmissiontable->get_name()); $ltitoolproxies = array_shift($itemcollection); $this->assertEquals('lti_tool_proxies', $ltitoolproxies->get_name()); $ltitypestable = array_shift($itemcollection); $this->assertEquals('lti_types', $ltitypestable->get_name()); $privacyfields = $ltisubmissiontable->get_privacy_fields(); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('datesubmitted', $privacyfields); $this->assertArrayHasKey('dateupdated', $privacyfields); $this->assertArrayHasKey('gradepercent', $privacyfields); $this->assertArrayHasKey('originalgrade', $privacyfields); $this->assertEquals('privacy:metadata:lti_submission', $ltisubmissiontable->get_summary()); $privacyfields = $ltitoolproxies->get_privacy_fields(); $this->assertArrayHasKey('name', $privacyfields); $this->assertArrayHasKey('createdby', $privacyfields); $this->assertArrayHasKey('timecreated', $privacyfields); $this->assertArrayHasKey('timemodified', $privacyfields); $this->assertEquals('privacy:metadata:lti_tool_proxies', $ltitoolproxies->get_summary()); $privacyfields = $ltitypestable->get_privacy_fields(); $this->assertArrayHasKey('name', $privacyfields); $this->assertArrayHasKey('createdby', $privacyfields); $this->assertArrayHasKey('timecreated', $privacyfields); $this->assertArrayHasKey('timemodified', $privacyfields); $this->assertEquals('privacy:metadata:lti_types', $ltitypestable->get_summary()); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // The LTI activity the user will have submitted something for. $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Another LTI activity that has no user activity. $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create a user which will make a submission. $user = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti->id, $user->id); // Check the contexts supplied are correct. $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(2, $contextlist); $contextformodule = $contextlist->current(); $cmcontext = \context_module::instance($lti->cmid); $this->assertEquals($cmcontext->id, $contextformodule->id); $contextlist->next(); $contextforsystem = $contextlist->current(); $this->assertEquals(SYSCONTEXTID, $contextforsystem->id); } /** * Test for provider::test_get_users_in_context() */ public function test_get_users_in_context(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $component = 'mod_lti'; // The LTI activity the user will have submitted something for. $lti1 = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Another LTI activity that has no user activity. $lti2 = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create user which will make a submission each. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti1->id, $user1->id); $this->create_lti_submission($lti1->id, $user2->id); $context = \context_module::instance($lti1->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); $this->assertCount(2, $userlist); $expected = [$user1->id, $user2->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertEquals($expected, $actual); $context = \context_module::instance($lti2->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); $this->assertEmpty($userlist); } /** * Test for provider::export_user_data(). */ public function test_export_for_context_submissions(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create users which will make submissions. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti->id, $user1->id); $this->create_lti_submission($lti->id, $user1->id); $this->create_lti_submission($lti->id, $user2->id); // Export all of the data for the context for user 1. $cmcontext = \context_module::instance($lti->cmid); $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_lti'); $writer = \core_privacy\local\request\writer::with_context($cmcontext); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data(); $this->assertCount(2, $data->submissions); } /** * Test for provider::export_user_data(). */ public function test_export_for_context_tool_types(): void { $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Create a user which will make a tool type. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create a user that will not make a tool type. $this->getDataGenerator()->create_user(); $type = new \stdClass(); $type->baseurl = 'http://moodle.org'; $type->course = $course1->id; lti_add_type($type, new \stdClass()); $type = new \stdClass(); $type->baseurl = 'http://moodle.org'; $type->course = $course1->id; lti_add_type($type, new \stdClass()); $type = new \stdClass(); $type->baseurl = 'http://moodle.org'; $type->course = $course2->id; lti_add_type($type, new \stdClass()); // Export all of the data for the context. $coursecontext = \context_course::instance($course1->id); $this->export_context_data_for_user($user->id, $coursecontext, 'mod_lti'); $writer = \core_privacy\local\request\writer::with_context($coursecontext); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data(); $this->assertCount(2, $data->lti_types); $coursecontext = \context_course::instance($course2->id); $this->export_context_data_for_user($user->id, $coursecontext, 'mod_lti'); $writer = \core_privacy\local\request\writer::with_context($coursecontext); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data(); $this->assertCount(1, $data->lti_types); } /** * Test for provider::export_user_data(). */ public function test_export_for_context_tool_proxies(): void { $this->resetAfterTest(); // Create a user that will not make a tool proxy. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $toolproxy = new \stdClass(); $toolproxy->createdby = $user; lti_add_tool_proxy($toolproxy); // Export all of the data for the context. $systemcontext = \context_system::instance(); $this->export_context_data_for_user($user->id, $systemcontext, 'mod_lti'); $writer = \core_privacy\local\request\writer::with_context($systemcontext); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data(); $this->assertCount(1, $data->lti_tool_proxies); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create users that will make submissions. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti->id, $user1->id); $this->create_lti_submission($lti->id, $user2->id); // Before deletion, we should have 2 responses. $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); $this->assertEquals(2, $count); // Delete data based on context. $cmcontext = \context_module::instance($lti->cmid); provider::delete_data_for_all_users_in_context($cmcontext); // After deletion, the lti submissions for that lti activity should have been deleted. $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); $this->assertEquals(0, $count); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create users that will make submissions. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti->id, $user1->id); $this->create_lti_submission($lti->id, $user2->id); // Before deletion we should have 2 responses. $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); $this->assertEquals(2, $count); $context = \context_module::instance($lti->cmid); $contextlist = new approved_contextlist($user1, 'lti', [\context_system::instance()->id, $context->id]); provider::delete_data_for_user($contextlist); // After deletion the lti submission for the first user should have been deleted. $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id, 'userid' => $user1->id]); $this->assertEquals(0, $count); // Check the submission for the other user is still there. $ltisubmission = $DB->get_records('lti_submission'); $this->assertCount(1, $ltisubmission); $lastsubmission = reset($ltisubmission); $this->assertEquals($user2->id, $lastsubmission->userid); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $component = 'mod_lti'; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create users that will make submissions. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $this->create_lti_submission($lti->id, $user1->id); $this->create_lti_submission($lti->id, $user2->id); $this->create_lti_submission($lti->id, $user3->id); // Before deletion we should have 2 responses. $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); $this->assertEquals(3, $count); $context = \context_module::instance($lti->cmid); $approveduserids = [$user1->id, $user2->id]; $approvedlist = new approved_userlist($context, $component, $approveduserids); provider::delete_data_for_users($approvedlist); // After deletion the lti submission for the first two users should have been deleted. list($insql, $inparams) = $DB->get_in_or_equal($approveduserids, SQL_PARAMS_NAMED); $sql = "ltiid = :ltiid AND userid {$insql}"; $params = array_merge($inparams, ['ltiid' => $lti->id]); $count = $DB->count_records_select('lti_submission', $sql, $params); $this->assertEquals(0, $count); // Check the submission for the third user is still there. $ltisubmission = $DB->get_records('lti_submission'); $this->assertCount(1, $ltisubmission); $lastsubmission = reset($ltisubmission); $this->assertEquals($user3->id, $lastsubmission->userid); } /** * Mimicks the creation of an LTI submission. * * There is no API we can use to insert an LTI submission, so we * will simply insert directly into the database. * * @param int $ltiid * @param int $userid */ protected function create_lti_submission(int $ltiid, int $userid) { global $DB; $ltisubmissiondata = [ 'ltiid' => $ltiid, 'userid' => $userid, 'datesubmitted' => time(), 'dateupdated' => time(), 'gradepercent' => 65, 'originalgrade' => 70, 'launchid' => 3, 'state' => 1 ]; $DB->insert_record('lti_submission', $ltisubmissiondata); } } tests/auth_test.php000064400000156303152265520010010425 0ustar00. namespace auth_lti; /** * Tests for the auth_plugin_lti class. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_plugin_lti */ final class auth_test extends \advanced_testcase { /** @var string issuer URL used for test cases. */ protected static string $issuer = 'https://lms.example.org'; /** @var int const representing cases where no PII is present. */ protected const PII_NONE = 0; /** @var int const representing cases where only names are included in PII. */ protected const PII_NAMES_ONLY = 1; /** @var int const representing cases where only email is included in PII. */ protected const PII_EMAILS_ONLY = 2; /** @var int const representing cases where both names and email are included in PII. */ protected const PII_ALL = 3; /** * Verify the user's profile picture has been set, which is useful to verify picture syncs. * * @param int $userid the id of the Moodle user. */ protected function verify_user_profile_image_updated(int $userid): void { global $CFG; $user = \core_user::get_user($userid); $usercontext = \context_user::instance($user->id); $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture; $page = new \moodle_page(); $page->set_url('/user/profile.php'); $page->set_context(\context_system::instance()); $renderer = $page->get_renderer('core'); $userpicture = new \user_picture($user); $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); } /** * Get a list of users ready for use with mock authentication requests by providing an array of user ids. * * @param array $ids the platform user_ids for the users. * @param string $role the LTI role to include in the user data. * @param bool $includenames whether to include the firstname and lastname of the user * @param bool $includeemail whether to include the email of the user * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). * @return array the users list. */ protected static function get_mock_users_with_ids( array $ids, string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', bool $includenames = true, bool $includeemail = true, bool $includepicture = false, ): array { $users = []; foreach ($ids as $id) { $user = [ 'user_id' => $id, 'given_name' => 'Firstname' . $id, 'family_name' => 'Surname' . $id, 'email' => "firstname.surname{$id}@lms.example.org", 'roles' => [$role] ]; if (!$includenames) { unset($user['given_name']); unset($user['family_name']); } if (!$includeemail) { unset($user['email']); } if ($includepicture) { $user['picture'] = self::getExternalTestFileUrl('/test.jpg'); } $users[] = $user; } return $users; } /** * Get a mock member structure based on a mock user and, optionally, a legacy user id. * * @param array $mockuser the user data * @param string $legacyuserid the id of the user in the platform in 1.1, if different from the id used in 1.3. * @return array */ protected function get_mock_member_data_for_user(array $mockuser, string $legacyuserid = ''): array { $data = [ 'user_id' => $mockuser['user_id'], 'roles' => $mockuser['roles'] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if (!empty($legacyuserid)) { $data['lti11_legacy_user_id'] = $legacyuserid; } return $data; } /** * Get mocked JWT data for the given user, including optionally the migration claim information if provided. * * @param array $mockuser the user data * @param array $mockmigration information needed to mock the migration claim * @return array the mock JWT data */ protected function get_mock_launchdata_for_user(array $mockuser, array $mockmigration = []): array { $data = [ 'iss' => self::$issuer, // Must match registration in create_test_environment. 'aud' => '123', // Must match registration in create_test_environment. 'sub' => $mockuser['user_id'], // User id on the platform site. 'exp' => time() + 60, 'nonce' => 'some-nonce-value-123', 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1', // Must match registration. 'https://purl.imsglobal.org/spec/lti/claim/roles' => $mockuser['roles'], 'https://purl.imsglobal.org/spec/lti/claim/resource_link' => [ 'title' => "Res link title", 'id' => 'res-link-id-123', ], "https://purl.imsglobal.org/spec/lti/claim/context" => [ "id" => "context-id-12345", "label" => "ITS 123", "title" => "ITS 123 Machine Learning", "type" => ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"] ], 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri' => 'https://this-moodle-tool.example.org/context/24/resource/14', 'https://purl.imsglobal.org/spec/lti/claim/custom' => [ 'id' => '1' ] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if ($mockmigration) { if (isset($mockmigration['consumer_key'])) { $base = [ $mockmigration['consumer_key'], $data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $data['iss'], $data['aud'], $data['exp'], $data['nonce'] ]; $basestring = implode('&', $base); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 'oauth_consumer_key' => $mockmigration['consumer_key'], ]; if (isset($mockmigration['signing_secret'])) { $sig = base64_encode(hash_hmac('sha256', $basestring, $mockmigration['signing_secret'])); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; } } if (isset($mockmigration['user_id'])) { $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['user_id'] = $mockmigration['user_id']; } } return $data; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_launch() method. * * @dataProvider launchdata_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $launchdata data describing the launch, including user data and migration claim data. * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_launch */ public function test_find_or_create_user_from_launch(?array $legacydata, array $launchdata, array $expected = []): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($launchdata['has_authenticated_before'])) { $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user']); $firstauthuser = $auth->find_or_create_user_from_launch($mockjwtdata); } // Create legacy users and mocked tool secrets if desired. $legacysecrets = []; if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } // In a real usage, legacy tool secrets are only passed for a consumer, as indicated in the migration claim. if (!empty($launchdata['migration_claim'])) { $legacysecrets = array_column($legacydata['tools'], 'secret'); } } // Mock the launchdata. $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user'], $launchdata['migration_claim'] ?? []); // Authenticate the platform user. $sink = $this->redirectEvents(); $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_launch($mockjwtdata, $legacysecrets); if (!empty($expected['migration_debugging'])) { $this->assertDebuggingCalled(); } $countusersafter = $DB->count_records('user'); $events = $sink->get_events(); $sink->close(); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated'])) ? 0 : 1; $numnewusers = (!empty($launchdata['has_authenticated_before'])) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); if (!empty($expected['migrated'])) { // If migrated, verify the user account is reusing the legacy user account. $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); $this->assertEmpty($events); // No updates as part of this method. } else if (isset($firstauthuser)) { // If the user is authenticating a second time, confirm the same account is being returned. $this->assertEquals($firstauthuser->id, $user->id); $this->assertEmpty($events); // No updates as part of this method. } else { // The user wasn't migrated and hasn't launched before, so we expect a user_created event. $this->assertInstanceOf(\core\event\user_created::class, $events[0]); } } /** * Data provider for testing launch-based authentication. * * @return array the test case data. */ public static function launchdata_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding names, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding emails, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding all PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, no migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null, ], 'expected' => [ 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration missing consumer_key' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration bad consumer_key' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_BAD', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration user not matched' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '234-bcd', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false ] ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim secret2' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration claim bad secret' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'bad_secret', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, no legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor including PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'Existing (linked) platform learner including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], ], 'Existing (linked) platform learner excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], ], 'Existing (linked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], ], 'Existing (linked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor excluding PII, picture included' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => false, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false, true )[0], 'migration_claim' => null ], ] ]; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_membership() method. * * @dataProvider membership_data_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $memberdata data describing the membership data, including user data and legacy user id info. * @param string $iss the issuer URL string * @param string|null $legacyconsumerkey optional legacy consumer_key value for testing user migration * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_membership */ public function test_find_or_create_user_from_membership(?array $legacydata, array $memberdata, string $iss, ?string $legacyconsumerkey, array $expected): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($memberdata['has_authenticated_before'])) { $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); $firstauthuser = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); } // Create legacy users and mocked tool secrets if desired. if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } } // Mock the membership data. $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); // Authenticate the platform user. $sink = $this->redirectEvents(); $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); $countusersafter = $DB->count_records('user'); $events = $sink->get_events(); $sink->close(); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated'])) ? 0 : 1; $numnewusers = (!empty($memberdata['has_authenticated_before'])) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $this->assertEquals($memberdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $this->assertEquals($memberdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; } if (!empty($expected['migrated'])) { // If migrated, verify the user account is reusing the legacy user account. $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } else if (isset($firstauthuser)) { // If the user is authenticating a second time, confirm the same account is being returned. $this->assertEquals($firstauthuser->id, $user->id); $this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update. } else { // The user wasn't migrated and hasn't launched before, so we expect a user_created event. $this->assertInstanceOf(\core\event\user_created::class, $events[0]); } } /** * Data provider for testing membership-service-based authentication. * * @return array the test case data. */ public static function membership_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ], 'New (unlinked) platform learner excluding names, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner excluding email, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NAMES_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id sent' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id omitted' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, no change in user id' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, unexpected consumer key bound, no change in user id' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_ABCDEF', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key not bound, legacy user id sent' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, no legacy data, consumer key bound, legacy user id sent' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ], 'Existing (linked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], ]; } /** * Test the behaviour of create_user_binding(). * * @covers ::create_user_binding */ public function test_create_user_binding(): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); $user = $this->getDataGenerator()->create_user(); $mockiss = self::$issuer; $mocksub = '1'; // Create a binding and verify it exists. $this->assertFalse($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); $auth->create_user_binding($mockiss, $mocksub, $user->id); $this->assertTrue($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); // Now, try to get an authenticated user USING that binding. Verify the bound user is returned. $numusersbefore = $DB->count_records('user'); $matcheduser = $auth->find_or_create_user_from_launch( $this->get_mock_launchdata_for_user( self::get_mock_users_with_ids([$mocksub])[0] ) ); $numusersafter = $DB->count_records('user'); $this->assertEquals($numusersafter, $numusersbefore); $this->assertEquals($user->id, $matcheduser->id); // Assert idempotency of the bind call. $this->assertNull($auth->create_user_binding($mockiss, $mocksub, $user->id)); } /** * Test updating a user account based on a given set of launchdata. * * @param array $firstlaunchdata the data from the first launch the user made. * @param array $launchdata the current launch data, which will dictate what data is updated. * @param array $expected array of test expectations * @dataProvider update_user_account_provider * @covers ::update_user_account */ public function test_update_user_account(array $firstlaunchdata, array $launchdata, array $expected): void { $this->resetAfterTest(); $auth = get_auth_plugin('lti'); // Mock the first authentication of the user. $firstmockjwtdata = $this->get_mock_launchdata_for_user($firstlaunchdata['user']); $user = $auth->find_or_create_user_from_launch($firstmockjwtdata); // Now, mock the recent authentication, confirming updates. $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user']); $sink = $this->redirectEvents(); $auth->update_user_account($user, $mockjwtdata, $mockjwtdata['iss']); $user = \core_user::get_user($user->id); $events = $sink->get_events(); $sink->close(); if (!empty($expected['user_updated'])) { $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } else { $this->assertEmpty($events); } // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $this->assertEquals($launchdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $this->assertEquals($launchdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; } // Verify picture sync occurs, if expected. if (!empty($expected['picture_updated'])) { $this->verify_user_profile_image_updated($user->id); } } /** * Data provider for testing user user_update_account. * * @return array the test case data. */ public static function update_user_account_provider(): array { return [ 'Full PII included in both auths, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => false, 'picture_updated' => false ] ], 'No PII included in both auths, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'expected' => [ 'PII' => self::PII_NONE, 'user_updated' => false, 'picture_updated' => false ] ], 'First auth no PII, second auth including PII, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth no PII, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'expected' => [ 'PII' => self::PII_NONE, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth emails only, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false )[0], ], 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth names only, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], 'expected' => [ 'PII' => self::PII_NAMES_ONLY, 'user_updated' => true, 'picture_updated' => false ] ], 'Full PII included in both auths, picture included in the second auth' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, true, true )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => false, 'picture_updated' => false ] ], ]; } } auth.php000064400000014077152265520010006225 0ustar00. /** * This file responds to a login authentication request * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); global $_POST, $_SERVER; if (!isloggedin() && empty($_POST['repost'])) { header_remove("Set-Cookie"); $PAGE->set_pagelayout('popup'); $PAGE->set_context(context_system::instance()); $output = $PAGE->get_renderer('mod_lti'); $page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST); echo $output->header(); echo $output->render($page); echo $output->footer(); return; } $scope = optional_param('scope', '', PARAM_TEXT); $responsetype = optional_param('response_type', '', PARAM_TEXT); $clientid = optional_param('client_id', '', PARAM_TEXT); $redirecturi = optional_param('redirect_uri', '', PARAM_URL); $loginhint = optional_param('login_hint', '', PARAM_TEXT); $ltimessagehintenc = optional_param('lti_message_hint', '', PARAM_TEXT); $state = optional_param('state', '', PARAM_TEXT); $responsemode = optional_param('response_mode', '', PARAM_TEXT); $nonce = optional_param('nonce', '', PARAM_TEXT); $prompt = optional_param('prompt', '', PARAM_TEXT); $ok = !empty($scope) && !empty($responsetype) && !empty($clientid) && !empty($redirecturi) && !empty($loginhint) && !empty($nonce); if (!$ok) { $error = 'invalid_request'; } $ltimessagehint = json_decode($ltimessagehintenc); $ok = $ok && isset($ltimessagehint->launchid); if (!$ok) { $error = 'invalid_request'; $desc = 'No launch id in LTI hint'; } if ($ok && ($scope !== 'openid')) { $ok = false; $error = 'invalid_scope'; } if ($ok && ($responsetype !== 'id_token')) { $ok = false; $error = 'unsupported_response_type'; } if ($ok) { $launchid = $ltimessagehint->launchid; list($courseid, $typeid, $id, $messagetype, $foruserid, $titleb64, $textb64) = explode(',', $SESSION->$launchid, 7); unset($SESSION->$launchid); $config = lti_get_type_type_config($typeid); $ok = ($clientid === $config->lti_clientid); if (!$ok) { $error = 'unauthorized_client'; } } if ($ok && ($loginhint !== $USER->id)) { $ok = false; $error = 'access_denied'; } // If we're unable to load up config; we cannot trust the redirect uri for POSTing to. if (empty($config)) { throw new moodle_exception('invalidrequest', 'error'); } else { $uris = array_map("trim", explode("\n", $config->lti_redirectionuris)); if (!in_array($redirecturi, $uris)) { throw new moodle_exception('invalidrequest', 'error'); } } if ($ok) { if (isset($responsemode)) { $ok = ($responsemode === 'form_post'); if (!$ok) { $error = 'invalid_request'; $desc = 'Invalid response_mode'; } } else { $ok = false; $error = 'invalid_request'; $desc = 'Missing response_mode'; } } if ($ok && !empty($prompt) && ($prompt !== 'none')) { $ok = false; $error = 'invalid_request'; $desc = 'Invalid prompt'; } if ($ok) { $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); if ($id) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); require_login($course, true, $cm); require_capability('mod/lti:view', $context); $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST); $lti->cmid = $cm->id; list($endpoint, $params) = lti_get_launch_data($lti, $nonce, $messagetype, $foruserid); } else { require_login($course); $context = context_course::instance($courseid); require_capability('moodle/course:manageactivities', $context); require_capability('mod/lti:addcoursetool', $context); // Set the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. $returnurlparams = [ 'course' => $courseid, 'id' => $typeid, 'sesskey' => sesskey() ]; $returnurl = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams); // Prepare the request. $title = base64_decode($titleb64); $text = base64_decode($textb64); $request = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, [], [], false, true, false, false, false, $nonce); $endpoint = $request->url; $params = $request->params; } } else { $params['error'] = $error; if (!empty($desc)) { $params['error_description'] = $desc; } } if (isset($state)) { $params['state'] = $state; } unset($SESSION->lti_message_hint); $r = '
\n"; if (!empty($params)) { foreach ($params as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); $r .= " \n"; } } $r .= "
\n"; $r .= "\n"; echo $r; services.php000064400000006015152265520610007106 0ustar00. /** * This file contains a controller for receiving LTI service requests * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $response = new \mod_lti\local\ltiservice\response(); $isget = $response->get_request_method() === mod_lti\local\ltiservice\resource_base::HTTP_GET; $isdelete = $response->get_request_method() === mod_lti\local\ltiservice\resource_base::HTTP_DELETE; if ($isget) { $response->set_accept(isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : ''); } else { $response->set_content_type(isset($_SERVER['CONTENT_TYPE']) ? explode(';', $_SERVER['CONTENT_TYPE'], 2)[0] : ''); } $ok = false; $path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : ''; $accept = $response->get_accept(); $contenttype = $response->get_content_type(); $services = lti_get_services(); foreach ($services as $service) { $resources = $service->get_resources(); foreach ($resources as $resource) { if (($isget && !empty($accept) && (strpos($accept, '*/*') === false) && !in_array($accept, $resource->get_formats())) || ((!$isget && !$isdelete) && !in_array($contenttype, $resource->get_formats()))) { continue; } $template = $resource->get_template(); $template = preg_replace('/{config_type}/', '(toolproxy|tool)', $template); $template = preg_replace('/\{[a-zA-Z_]+\}/', '[^/]+', $template); $template = preg_replace('/\(([0-9a-zA-Z_\-,\/]+)\)/', '(\\1|)', $template); $template = str_replace('/', '\/', $template); if (preg_match("/^{$template}$/", $path) === 1) { $ok = true; break 2; } } } if (!$ok) { $response->set_code(400); $response->set_reason("No handler found for {$path} {$accept} {$contenttype}"); } else { $body = file_get_contents('php://input'); $response->set_request_data($body); if (in_array($response->get_request_method(), $resource->get_methods())) { $resource->execute($response); } else { $response->set_code(405); } } $response->send(); styles.css000064400000022611152265520610006607 0ustar00.path-mod-lti .ltiframe { position: relative; width: 100%; height: 100%; } /** General Styles **/ .path-mod-lti .userpicture, .path-mod-lti .picture.user, .path-mod-lti .picture.teacher { width: 35px; height: 35px; vertical-align: top; } .path-mod-lti .feedback .files, .path-mod-lti .feedback .grade, .path-mod-lti .feedback .outcome, .path-mod-lti .feedback .finalgrade { float: right; } .path-mod-lti .feedback .disabledfeedback { width: 500px; height: 250px; } .path-mod-lti .feedback .from { float: left; } .path-mod-lti .files img { margin-right: 4px; } .path-mod-lti .files a { white-space: nowrap; } .path-mod-lti .late { color: red; } /* Styles for admin */ .path-admin-mod-lti .mform .fitem .fitemtitle { min-width: 18em; padding-right: 1em; } /* Prevent setting titles from wrapping */ /* Styles for instructor editing an external tool */ .path-mod-lti .mform .fitem .fitemtitle { min-width: 14em; padding-right: 1em; } /* Prevent setting titles from wrapping */ /* Styling for tool_configure page */ #registration-choice-container .buffer-text { margin: 20px; } #choice-list { list-style: none; border-bottom: 1px solid #e3e3e3; padding-bottom: 1em; margin-left: 0; } #choice-list > li { display: inline-block; } #external-registration-container iframe { border: 1px solid #e5e5e5; border-radius: 10px; width: 100%; min-height: 800px; } .loading-screen { text-align: center; padding: 3em; } .loading-screen .loading-text { font-size: 2em; } .loading-screen .loader { margin-left: auto; margin-right: auto; margin-bottom: 1em; height: 2em; width: 2em; font-size: 2em; } #registration-submit { min-width: 140px; } #registration-form-container { min-height: 260px; } #registration-form-container .card { margin-bottom: 0; } #registration-form-container .control-group:last-child { margin-bottom: 0; } #registration-choice-container .card { text-align: center; } #registration-choice-container .btn-toolbar { margin-bottom: 0; } #registration-choice-container p:last-child { margin-top: 20px; } #tool-type-capabilities-container .registration-loading-container { display: none; } #tool-type-capabilities-container.loading .registration-loading-container { display: block; } #tool-type-capabilities-container.loading #tool-type-capabilities-template-container { display: none; } .centered-menu { max-width: 70%; margin-left: auto; margin-right: auto; } .btn-text { display: block; } .btn-loader { display: none; } .loading .btn-text { display: none; } .loading .btn-loader { display: block; } .btn .loader { margin-left: auto; margin-right: auto; } .btn .loader img { height: 1.5em; } #tool-list-container h3 { display: inline-block; } #tool-list-loader-container { display: inline-block; } #tool-list-loader-container .loader { display: none; } #tool-list-loader-container .loader img { height: 2em; } .loading #tool-list-loader-container .loader { display: block; } .loading #tool-notools-text { display: none; } .tool-card { display: inline-block; width: 250px; height: 300px; border: 1px solid #e5e5e5; border-radius: 10px; margin: 5px; position: relative; box-sizing: border-box; vertical-align: top; } .tool-card:hover, .tool-card:focus { border-color: #08c; box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; } .tool-card .overlay-container { background-color: rgba(255, 255, 255, 0.8); border-radius: 10px; display: none; height: 100%; left: 0; position: absolute; text-align: center; top: 0; width: 100%; z-index: 100; box-sizing: border-box; padding: 10px; } .tool-card .overlay-container .img-container { position: absolute; top: 115px; left: 90px; display: block; width: 70px; height: 70px; } .tool-card .overlay-container .img-container img { height: 100%; width: 100%; } .tool-card.announcement > .overlay-container { display: block; } .tool-card.announcement .overlay-container .loader { display: none; } .tool-card.announcement.loading .overlay-container .loader { display: block; width: 100%; height: 100%; } .tool-card.announcement .overlay-container .success-icon-container { display: none; } .tool-card.announcement.success .overlay-container .success-icon-container { display: block; } .tool-card.announcement .overlay-container .fail-icon-container { display: none; } .tool-card.announcement.fail .overlay-container .fail-icon-container { display: block; } .tool-card.announcement .overlay-container .capabilities-container { display: none; } .tool-card.announcement.capabilities .overlay-container .capabilities-container { display: block; } .tool-card.announcement.capabilities .overlay-container { background-color: rgb(255, 255, 255); } .tool-card.announcement.capabilities .overlay-container .img-container { display: none; } .tool-card-content { z-index: 1; } .tool-card-header { text-align: center; background-color: #f5f5f5; padding: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; box-sizing: border-box; height: 125px; } .tool-card-subheader { margin-bottom: 10px; text-align: left; } .tool-card-header .tool-card-icon { width: 35px; height: 35px; } .tool-card-header .name { margin-bottom: 0; white-space: nowrap; } .tool-card-header .tool-card-actions { float: right; } .tool-card-header .tool-card-actions img { width: 15px; height: 15px; margin-left: 7px; } .tool-card-body { border-top: 1px solid #e5e5e5; box-sizing: border-box; padding: 5px; height: 125px; } .tool-card-body .description { max-height: 100px; word-wrap: break-word; } .tool-card-footer { height: 50px; text-align: center; padding-top: 10px; box-sizing: border-box; } .tool-card .contenteditable-container { position: relative; } .tool-card [contenteditable=true] { border: 1px solid transparent; padding: 0.25em; position: relative; z-index: 1; overflow: auto; } .tool-card [contenteditable=true]:hover { border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); background-color: #fff; border: 1px solid #e3e3e3; -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; cursor: text; } .tool-card [contenteditable=true]:focus { outline: 0; border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); background-color: #fff; border: 1px solid rgba(82, 168, 236, 0.8); -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; cursor: text; } .tool-card [contenteditable=true].loading + .overlay-container { border-radius: 4px; display: block; } .tool-card [contenteditable=true] + .overlay-container .loader { display: inline-block; vertical-align: middle; } #accordion.modltitree { border-radius: 4px; padding-left: 8px; } #accordion.modltitree .accordion-inner { padding-left: 20px; } #accordion.modltitree .accordion-inner .accordion p { margin-bottom: 0; padding-left: 15px; } #accordion.modltitree .accordion-group { padding-left: 15px; padding-bottom: 4px; position: relative; } #accordion.modltitree .accordion-group span { padding: 0 4px 0; } #accordion.modltitree .accordion-group .accordion-heading .accordion-toggle::before { content: url([[pix:t/expanded]]); position: absolute; left: -5px; } #accordion.modltitree .accordion-group .accordion-heading .accordion-toggle.collapsed::before { content: url([[pix:t/collapsed]]); } /* Styling for LTI view */ #contentframe { border: 1px solid #ddd; border-radius: 4px; } /* Strip the caret from the action menu toggle, as the ... ellipsis icon is used. */ #page-mod-lti-coursetools a.dropdown-toggle::after { display: none; } /* Icons in the 'actions' column of the course tools table need to be aligned with action menus which have a 36x36 trigger. */ #page-mod-lti-coursetools table div.tool-action-icon-container { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; } #page-mod-lti-coursetools table div.tool-action-icon-container i { margin: 0; } /* The following removes the striping that lives in table_sql, which we can't change from within reportbuilder */ #page-mod-lti-coursetools table.generaltable tbody tr:nth-of-type(2n+1), #page-mod-lti-coursetools table.table-striped tbody tr:nth-of-type(2n+1) { background: inherit; } #page-mod-lti-coursetools table.generaltable tbody tr:hover, #page-mod-lti-coursetools table.table-striped tbody tr:hover { background: #fff; } db/services.php000064400000014641152265520610007477 0ustar00. /** * External tool external functions and service definitions. * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ defined('MOODLE_INTERNAL') || die; $functions = array( 'mod_lti_get_tool_launch_data' => array( 'classname' => 'mod_lti_external', 'methodname' => 'get_tool_launch_data', 'description' => 'Return the launch data for a given external tool.', 'type' => 'read', 'capabilities' => 'mod/lti:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_lti_get_ltis_by_courses' => array( 'classname' => 'mod_lti_external', 'methodname' => 'get_ltis_by_courses', 'description' => 'Returns a list of external tool instances in a provided set of courses, if no courses are provided then all the external tool instances the user has access to will be returned.', 'type' => 'read', 'capabilities' => 'mod/lti:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_lti_view_lti' => array( 'classname' => 'mod_lti_external', 'methodname' => 'view_lti', 'description' => 'Trigger the course module viewed event and update the module completion status.', 'type' => 'read', 'capabilities' => 'mod/lti:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_lti_get_tool_proxies' => array( 'classname' => 'mod_lti_external', 'methodname' => 'get_tool_proxies', 'description' => 'Get a list of the tool proxies', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_create_tool_proxy' => array( 'classname' => 'mod_lti_external', 'methodname' => 'create_tool_proxy', 'description' => 'Create a tool proxy', 'type' => 'write', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_delete_tool_proxy' => array( 'classname' => 'mod_lti_external', 'methodname' => 'delete_tool_proxy', 'description' => 'Delete a tool proxy', 'type' => 'write', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_get_tool_proxy_registration_request' => array( 'classname' => 'mod_lti_external', 'methodname' => 'get_tool_proxy_registration_request', 'description' => 'Get a registration request for a tool proxy', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_get_tool_types' => array( 'classname' => 'mod_lti_external', 'methodname' => 'get_tool_types', 'description' => 'Get a list of the tool types', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_get_tool_types_and_proxies' => [ 'classname' => 'mod_lti\external\get_tool_types_and_proxies', 'methodname' => 'execute', 'description' => 'Get a list of the tool types and tool proxies', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ], 'mod_lti_get_tool_types_and_proxies_count' => [ 'classname' => 'mod_lti\external\get_tool_types_and_proxies_count', 'methodname' => 'execute', 'description' => 'Get total number of the tool types and tool proxies', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ], 'mod_lti_create_tool_type' => array( 'classname' => 'mod_lti_external', 'methodname' => 'create_tool_type', 'description' => 'Create a tool type', 'type' => 'write', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_update_tool_type' => array( 'classname' => 'mod_lti_external', 'methodname' => 'update_tool_type', 'description' => 'Update a tool type', 'type' => 'write', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_delete_tool_type' => array( 'classname' => 'mod_lti_external', 'methodname' => 'delete_tool_type', 'description' => 'Delete a tool type', 'type' => 'write', 'capabilities' => 'moodle/site:config', 'ajax' => true ), 'mod_lti_delete_course_tool_type' => array( 'classname' => 'mod_lti\external\delete_course_tool_type', 'description' => 'Delete a course tool type', 'type' => 'write', 'capabilities' => 'mod/lti:addcoursetool', 'ajax' => true ), 'mod_lti_toggle_showinactivitychooser' => array( 'classname' => 'mod_lti\external\toggle_showinactivitychooser', 'description' => 'Toggle showinactivitychooser for a tool type in a course', 'type' => 'write', 'capabilities' => 'mod/lti:addcoursetool', 'ajax' => true ), 'mod_lti_is_cartridge' => array( 'classname' => 'mod_lti_external', 'methodname' => 'is_cartridge', 'description' => 'Determine if the given url is for a cartridge', 'type' => 'read', 'capabilities' => 'moodle/site:config', 'ajax' => true ), ); db/subplugins.json000064400000000314152265520610010221 0ustar00{ "subplugintypes": { "ltisource": "source", "ltiservice": "service" }, "plugintypes": { "ltisource": "mod/lti/source", "ltiservice": "mod/lti/service" } } db/install.php000064400000002342152265520610007315 0ustar00. /** * Post installation and migration code. * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Stub for database installation. */ function xmldb_lti_install() { global $CFG, $OUTPUT; // Create the private key. require_once($CFG->dirroot . '/mod/lti/upgradelib.php'); $warning = mod_lti_verify_private_key(); if (!empty($warning)) { echo $OUTPUT->notification($warning, 'notifyproblem'); } } db/log.php000064400000002377152265520610006440 0ustar00. /** * LTI web service endpoints * * @package mod_lti * @category log * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Chris Scribner */ defined('MOODLE_INTERNAL') || die(); $logs = array( array('module' => 'lti', 'action' => 'view', 'mtable' => 'lti', 'field' => 'name'), array('module' => 'lti', 'action' => 'launch', 'mtable' => 'lti', 'field' => 'name'), array('module' => 'lti', 'action' => 'view all', 'mtable' => 'lti', 'field' => 'name') );db/tasks.php000064400000002236152265520610006776 0ustar00. /** * This file defines tasks performed by the plugin. * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // List of tasks. $tasks = array( array( 'classname' => 'mod_lti\task\clean_access_tokens', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ) ); db/access.php000064400000010523152265520610007110 0ustar00. /** * This file contains the capabilities used by the lti module * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis, marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $capabilities = array( // Whether the user can see the link to the external tool and follow it. 'mod/lti:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), // Add an External tool activity to a course. 'mod/lti:addinstance' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), // When the user arrives at the external tool, if they have this capability // in Moodle, then they are given the Instructor role in the remote system, // otherwise they are given Learner. See the lti_get_ims_role function. 'mod/lti:manage' => array( 'riskbitmask' => RISK_PERSONAL, // A bit of a guess, but seems likely. 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), // When the user arrives at the external tool, if they have this capability // in Moodle, then they are given the Administrator role in the remote system, // otherwise they are given Learner. See the lti_get_ims_role function. 'mod/lti:admin' => array( 'riskbitmask' => RISK_PERSONAL, // A bit of a guess, but seems likely. 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE ), // The ability to create or edit tool configurations for particular courses. 'mod/lti:addcoursetool' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), // The ability to a preconfigured instance to the course. 'mod/lti:addpreconfiguredinstance' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/lti:addinstance', ), // The ability to request the administrator to configure a particular // External tool globally. 'mod/lti:requesttooladd' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); $deprecatedcapabilities = [ // The ability to add a manual instance (i.e. not from a preconfigured tool) to the course. 'mod/lti:addmanualinstance' => [ 'message' => 'Manual instance configuration is deprecated. Please create a course tool (mod/lti:addcoursetool) and ensure '. 'users are able to add an instance of the course tool via the activity chooser (mod/lti:addpreconfiguredinstance).' ], ]; db/caches.php000064400000002065152265520610007077 0ustar00. /** * This file contains the cache definitions for the lti plugin * * @package mod_lti * @copyright 2020 Carlos Vinícius Monteiro Costa * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // Added definition for keyset cache. $definitions = [ 'keyset' => [ 'mode' => cache_store::MODE_APPLICATION ] ];lang/en/lti.php000064400000130130152265520610007372 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * Strings for component 'mod_lti', language 'en'. * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $string['accept'] = 'Accept'; $string['accepted'] = 'Accepted'; $string['accept_grades'] = 'Accept grades from the tool'; $string['accept_grades_admin'] = 'Accept grades from the tool'; $string['accept_grades_admin_help'] = 'Specify whether the tool provider can add, update, read, and delete grades associated with instances of this tool. Some tool providers support reporting grades back to Moodle based on actions taken within the tool, creating a more integrated experience.'; $string['accept_grades_help'] = 'Specify whether the tool provider can add, update, read, and delete grades associated only with this external tool instance. Some tool providers support reporting grades back to Moodle based on actions taken within the tool, creating a more integrated experience. Note that this setting may be overridden in the tool configuration.'; $string['accept_grades_from_tool'] = 'Allow {$a} to add grades in the gradebook'; $string['action'] = 'Action'; $string['activate'] = 'Activate'; $string['activatetoadddescription'] = 'You will need to activate this tool before you can add a description.'; $string['active'] = 'Active'; $string['activity'] = 'Activity'; $string['add_ltiadv'] = 'Add LTI Advantage'; $string['add_ltilegacy'] = 'Add Legacy LTI'; $string['addnewapp'] = 'Enable external application'; $string['addserver'] = 'Add new trusted server'; $string['addtool'] = 'Add tool'; $string['addtype'] = 'Add preconfigured tool'; $string['allow'] = 'Allow'; $string['allowsetting'] = 'Allow tool to store 8K of settings in Moodle'; $string['always'] = 'Always'; $string['automatic'] = 'Automatic, based on tool URL'; $string['autoaddtype'] = 'Add tool'; $string['baseurl'] = 'Base URL/tool registration name'; $string['basiclti'] = 'LTI'; $string['basiclti_base_string'] = 'LTI OAuth base string'; $string['basiclti_endpoint'] = 'LTI launch endpoint'; $string['basiclti_in_new_window'] = 'Your activity has opened in a new window'; $string['basiclti_in_new_window_open'] = 'Open in new window'; $string['basiclti_parameters'] = 'LTI launch parameters'; $string['basicltiactivities'] = 'LTI activities'; $string['basicltifieldset'] = 'Custom example fieldset'; $string['basicltiintro'] = 'Activity description'; $string['basicltiname'] = 'Activity name'; $string['basicltisettings'] = 'Basic Learning Tool Interoperability (LTI) settings'; $string['cachedef_keyset'] = 'Caches the keyset information of tools'; $string['cancel'] = 'Cancel'; $string['cancelled'] = 'Cancelled'; $string['capabilities'] = 'Capabilities'; $string['capabilitiesrequired'] = 'This tool requires access to the following data in order to activate:'; $string['capabilities_help'] = 'Select those capabilities which you wish to offer to the tool provider. More than one capability can be selected.'; $string['cleanaccesstokens'] = 'External tool removal of expired access tokens'; $string['click_to_continue'] = 'Click to continue'; $string['clientidadmin'] = 'Client ID'; $string['clientidadmin_help'] = 'The client ID is a unique value used to identify a tool. It is created automatically for each tool which uses the JWT security profile introduced in LTI 1.3 and should be part of the details passed to the tool provider so that they can configure the connection at their end.'; $string['comment'] = 'Comment'; $string['configpassword'] = 'Default remote tool password'; $string['configpreferheight'] = 'Default preferred height'; $string['configpreferwidget'] = 'Set widget as default launch'; $string['configpreferwidth'] = 'Default preferred width'; $string['configresourceurl'] = 'Default resource URL'; $string['configtoolurl'] = 'Default remote tool URL'; $string['configtypes'] = 'Enable LTI applications'; $string['configured'] = 'Configured'; $string['confirmtoolactivation'] = 'Are you sure you would like to activate this tool?'; $string['contentitem_deeplinking'] = 'Supports Deep Linking (Content-Item Message)'; $string['contentitem_deeplinking_help'] = 'If ticked, the option \'Select content\' will be available when adding an external tool.'; $string['contentitem_multiple_description'] = 'The following items will be added to your course:'; $string['contentitem_multiple_graded'] = 'Graded activity (Maximum grade: {$a})'; $string['contentselected'] = 'Content selected'; $string['courseactivitiesorresources'] = 'Course activities or resources'; $string['courseexternaltooladd'] = 'Add new LTI External tool'; $string['courseexternaltooladdsuccess'] = '{$a} added.'; $string['courseexternaltooledit'] = 'Edit {$a}'; $string['courseexternaltooleditsuccess'] = 'Changes saved.'; $string['courseexternaltooliconalt'] = 'Icon for {$a}'; $string['courseexternaltools'] = 'LTI External tools'; $string['courseexternaltoolsinfo'] = 'LTI External tools are add-on apps you can integrate into your course, such as interactive content or assessments. Your students can access and use them without leaving your course.'; $string['courseexternaltoolsnoeditpermissions'] = 'You don\'t have permission to edit this tool'; $string['courseexternaltoolsnoviewpermissions'] = 'View course external tools'; $string['courseid'] = 'Course ID number'; $string['courseinformation'] = 'Course information'; $string['courselink'] = 'Go to course'; $string['coursemisconf'] = 'Course is misconfigured'; $string['coursetooldeleted'] = '{$a} deleted'; $string['createdon'] = 'Created on'; $string['curllibrarymissing'] = 'PHP cURL extension required for the External tool.'; $string['custom'] = 'Custom parameters'; $string['custom_help'] = 'Custom parameters are settings used by the tool provider. For example, a custom parameter may be used to display a specific resource from the provider. Each parameter should be entered on a separate line using a format of "name=value"; for example, "chapter=3". It is safe to leave this field unchanged unless directed by the tool provider.'; $string['custominstr'] = 'Custom parameters'; $string['debuglaunch'] = 'Debug option'; $string['debuglaunchoff'] = 'Normal launch'; $string['debuglaunchon'] = 'Debug launch'; $string['default'] = 'Default'; $string['default_launch_container'] = 'Default launch container'; $string['default_launch_container_help'] = 'The launch container affects the display of the tool when launched from the course. Some launch containers provide more screen real estate to the tool, and others provide a more integrated feel with the Moodle environment. * **Default** - Use the launch container specified by the tool configuration. * **Embed** - The tool is displayed within the existing Moodle window, in a manner similar to most other Activity types. * **Embed, without blocks** - The tool is displayed within the existing Moodle window, with just the navigation controls at the top of the page. * **New window** - The tool opens in a new window, occupying all the available space. Depending on the browser, it will open in a new tab or a popup window. It is possible that browsers will prevent the new window from opening.'; $string['delegate'] = 'Delegate to teacher'; $string['delegate_tool'] = 'As specified in Deep Linking definition or Delegate to teacher'; $string['delete'] = 'Delete'; $string['delete_confirmation'] = 'Are you sure you want to delete this preconfigured tool?'; $string['deletecoursetool'] = 'Delete {$a}'; $string['deletecoursetoolconfirm'] = 'This will delete {$a} from the available LTI tools in your course.'; $string['deletecoursetoolwithusageconfirm'] = '{$a} is currently being used in at least one activity in your course. If you delete this tool, the activities that use it will no longer work.

Are you sure you want to delete {$a}?'; $string['display_description'] = 'Display activity description when students access the tool'; $string['display_description_help'] = 'Content from this tool is displayed embedded in a page in the course. This setting determines if the activity description is shown in that page.'; $string['display_name'] = 'Display activity name when students access the tool'; $string['display_name_help'] = 'Content from this tool is displayed embedded in a page in the course. This setting determines if the activity name is shown in that page.'; $string['donot'] = 'Do not send'; $string['donotaccept'] = 'Do not accept'; $string['donotallow'] = 'Do not allow'; $string['dynreg_update_text'] = 'There are existing tools attached to the registration\'s domain. Do you want to update an already installed external tool or create a new external tool?'; $string['dynreg_update_warn_dupdomain'] = 'It is not recommended to have multiple external tools under the same domain.'; $string['dynreg_update_name'] = 'Tool name'; $string['dynreg_update_url'] = 'Base URL'; $string['dynreg_update_version'] = 'LTI version'; $string['dynreg_update_notools'] = 'No tools in context.'; $string['dynreg_update_btn_update'] = 'Update'; $string['dynreg_update_btn_new'] = 'Register as a new external tool'; $string['duplicateregurl'] = 'This registration URL is already in use'; $string['editdescription'] = 'Click here to give this tool a description'; $string['editmanualinstancedeprecationwarning'] = 'Manually configured External tool activities are no longer supported. Don\'t worry, this activity will still work as it is, but you can\'t make changes to the tool configuration here anymore.

To make any changes to the tool, or to create new activities with it, the tool needs to be added to your course in Course > More > LTI External tools. Then, you will be able to create new activities, selecting the tool directly in the Activity chooser.

You can read more about adding LTI External tools in the documentation External tool.'; $string['embed'] = 'Embed'; $string['embed_no_blocks'] = 'Embed, without blocks'; $string['enableemailnotification'] = 'Send notification emails'; $string['enableemailnotification_help'] = 'If enabled, students will receive email notification when their tool submissions are graded.'; $string['enterkeyandsecret'] = 'Enter your consumer key and shared secret'; $string['enterkeyandsecret_help'] = 'If you were given a consumer key and/or shared secret, input them here'; $string['entitycourseexternaltools'] = 'LTI External tools'; $string['errorbadurl'] = 'URL is not a valid tool URL or cartridge.'; $string['errorincorrectconsumerkey'] = 'Consumer key is incorrect.'; $string['errorinvaliddata'] = 'Invalid data: {$a}'; $string['errorinvalidmediatype'] = 'Invalid media type: {$a}'; $string['errorinvalidresponseformat'] = 'Invalid Content-Item response format.'; $string['errormisconfig'] = 'Misconfigured tool. Please ask your Moodle administrator to fix the configuration of the tool.'; $string['errortooltypenotfound'] = 'LTI tool type not found.'; $string['existing_window'] = 'Existing window'; $string['extensions'] = 'LTI extension services'; $string['external_tool_type'] = 'Preconfigured tool'; $string['external_tool_type_help'] = '* **Automatic, based on tool URL** - The best tool configuration is selected automatically. If the tool URL is not recognised, the tool configuration details may need to be entered manually. * **A specific preconfigured tool** - The tool configuration for the specified tool will be used when communicating with the external tool provider. If the tool URL does not appear to belong to the tool provider, a warning will be shown. It is not always necessary to enter a tool URL. * **Custom configuration** - A consumer key and shared secret may need to be entered manually. The consumer key and shared secret may be obtained from the tool provider. However, not all tools require a consumer key and shared secret, in which case the fields may be left blank. ### Preconfigured tool editing Three icons are available after the preconfigured tool drop-down menu: * **Add** - Create a course level tool configuration. All External tool instances in this course may use the tool configuration. * **Edit** - Select a course level tool from the drop-down menu, then click this icon. The details of the tool configuration may be edited. * **Delete** - Remove the selected course level tool.'; $string['external_tool_types'] = 'Preconfigured tools'; $string['failedtoconnect'] = 'Moodle was unable to communicate with the "{$a}" system'; $string['failedtocreatetooltype'] = 'Failed to create new tool. Please check the URL and try again.'; $string['failedtodeletetoolproxy'] = 'Failed to delete tool registration. You may need to visit "Manage external tool registrations" and delete it manually.'; $string['filter_basiclti_configlink'] = 'Configure your preferred sites and their passwords'; $string['filter_basiclti_password'] = 'Password is mandatory'; $string['filterconfig'] = 'LTI administration'; $string['filtername'] = 'LTI'; $string['fixexistingconf'] = 'Use an existing configuration for the misconfigured instance'; $string['fixnew'] = 'New configuration'; $string['fixnewconf'] = 'Define a new configuration for the misconfigured instance'; $string['fixold'] = 'Use existing'; $string['force_ssl'] = 'Force SSL'; $string['force_ssl_help'] = 'Selecting this option forces all launches to this tool provider to use SSL. In addition, all web service requests from the tool provider will use SSL. If using this option, confirm that this Moodle site and the tool provider support SSL.'; $string['generaltool'] = 'General tool'; $string['grading'] = 'Grade routing'; $string['icon_url'] = 'Icon URL'; $string['icon_url_help'] = 'The icon URL allows the icon that shows up in the course listing for this activity to be modified. Instead of using the default LTI icon, an icon which conveys the type of activity may be specified.'; $string['id'] = 'ID'; $string['indicator:cognitivedepth'] = 'LTI cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in an LTI activity.'; $string['indicator:cognitivedepthdef'] = 'LTI cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the LTI activities during this analysis interval (Levels = No view, View, Submit, View feedback)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'LTI social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in an LTI activity.'; $string['indicator:socialbreadthdef'] = 'LTI social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the LTI activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['initiatelogin'] = 'Initiate login URL'; $string['initiatelogin_help'] = 'The tool URL to which requests for initiating a login are to be sent. This URL is required before a message can be successfully sent to the tool.'; $string['invalidid'] = 'LTI ID was incorrect'; $string['jwtsecurity'] = 'LTI 1.3'; $string['keytype'] = 'Public key type'; $string['keytype_help'] = 'The authentication method used to validate the tool.'; $string['keytype_keyset'] = 'Keyset URL'; $string['keytype_rsa'] = 'RSA key'; $string['launch_in_moodle'] = 'Launch tool in Moodle'; $string['launch_in_popup'] = 'Launch tool in a pop-up'; $string['launch_url'] = 'Tool URL'; $string['launch_url_help'] = 'The tool URL indicates the web address of the External Tool, and may contain additional information, such as the resource to show. If you are unsure what to enter for the tool URL, please check with the tool provider for more information. You can enter a cartridge URL if you have one, and the rest of the form details will fill automatically. If you have selected a preconfigured tool, you may not need to enter a tool URL. If the tool link is used to just launch into the tool provider\'s system, and not go to a specific resource, this will likely be the case.'; $string['launchinpopup'] = 'Launch container'; $string['launchinpopup_help'] = 'The launch container affects the display of the tool when launched from the course. Some launch containers provide more screen real estate to the tool, and others provide a more integrated feel with the Moodle environment. * **Default** - Use the launch container specified by the tool configuration. * **Embed** - The tool is displayed within the existing Moodle window, in a manner similar to most other Activity types. * **Embed, without blocks** - The tool is displayed within the existing Moodle window, with just the navigation controls at the top of the page. * **New window** - The tool opens in a new window, occupying all the available space. Depending on the browser, it will open in a new tab or a popup window. It is possible that browsers will prevent the new window from opening.'; $string['launchoptions'] = 'Launch options'; $string['lti'] = 'LTI'; $string['lti:addcoursetool'] = 'Add course-specific tool configurations'; $string['lti:addmanualinstanceprohibitederror'] = 'The manual creation of tools without a course tool definition is no longer supported. Please create a course tool first and then use that to create activity instances.'; $string['lti:addinstance'] = 'Add a new external tool'; $string['lti:addpreconfiguredinstance'] = 'Add a preconfigured tool'; $string['lti:grade'] = 'View grades returned by the external tool'; $string['lti:manage'] = 'Be an Instructor when the tool is launched'; $string['lti:admin'] = 'Be an administrator when the tool is launched'; $string['lti:requesttooladd'] = 'Request a tool is configured site-wide'; $string['lti:view'] = 'Launch external tool activities'; $string['ltisettings'] = 'LTI settings'; $string['lti_administration'] = 'Edit preconfigured tool'; $string['lti_errormsg'] = 'The tool returned the following error message: "{$a}"'; $string['lti_launch_error'] = 'An error occurred when launching the external tool:'; $string['lti_launch_error_tool_request'] = '

To submit a request for an administrator to complete the tool configuration, click here.

'; $string['lti_launch_error_unsigned_help'] = '

This error may be a result of a missing consumer key and shared secret for the tool provider.

If you have a consumer key and shared secret, you may enter it when editing the external tool instance (make sure advanced options are visible).

Alternatively, you may create a course level tool provider configuration.

'; $string['lti_tool_request_added'] = 'Tool configuration request successfully submitted. You may need to contact an administrator to complete the tool configuration.'; $string['lti_tool_request_existing'] = 'A tool configuration for the tool domain has already been submitted.'; $string['ltiunknownserviceapicall'] = 'LTI unknown service API call.'; $string['ltiversion'] = 'LTI version'; $string['ltiversion_help'] = 'The version of LTI being used for signing messages and service requests: LTI 1.0/1.1 and LTI 2.0 use the OAuth 1.0A security profile; LTI 1.3.0 uses JWTs.'; $string['main_admin'] = 'General help'; $string['main_admin_help'] = 'External tools allow Moodle users to interact with learning resources hosted remotely. Through a special launch protocol, the remote tool has access to general information about the launching user. For example, the institution name, course ID, user ID, and other information such as the user\'s name or email address. Tools listed on this page are separated into three categories: * **Active** - These tool providers have been approved and configured by an administrator. They can be used from within any course on this site. If a consumer key and shared secret are entered, a trust relationship is established between this site and the remote tool, providing a secure communication channel. * **Pending** - These tool providers came in through a package import, but have not been configured by an administrator. Teachers may still use tools from these providers if they have a consumer key and shared secret, or if none is required. * **Rejected** - These tools providers are flagged as ones which an administrator has no intention of making available on the site. Teachers may still use tools from these providers if they have a consumer key and shared secret, or if none is required.'; $string['manage_external_tools'] = 'Manage tools'; $string['manage_tools'] = 'Manage preconfigured tools'; $string['manage_tool_proxies'] = 'Manage external tool registrations'; $string['manuallyaddtype'] = 'Alternatively, you can configure a tool manually.'; $string['miscellaneous'] = 'Miscellaneous'; $string['misconfiguredtools'] = 'Misconfigured tool instances were detected'; $string['missingparameterserror'] = 'The page is misconfigured: "{$a}"'; $string['module_class_type'] = 'Moodle module type'; $string['modulename'] = 'External tool'; $string['modulename_help'] = 'The external tool activity module enables students to interact with learning resources and activities on other web sites. For example, an external tool could provide access to a new activity type or learning materials from a publisher. To create an external tool activity, a tool provider which supports LTI (Learning Tools Interoperability) is required. A teacher can create an external tool activity or make use of a tool configured by the site administrator. External tool activities differ from URL resources in a few ways: * External tools are context aware i.e. they have access to information about the user who launched the tool, such as institution, course and name * External tools support reading, updating, and deleting grades associated with the activity instance * External tool configurations create a trust relationship between your site and the tool provider, allowing secure communication between them'; $string['modulename_link'] = 'mod/lti/view'; $string['modulename_shortcut_link'] = 'mod/lti/view/custom'; $string['modulenameplural'] = 'External tools'; $string['modulenamepluralformatted'] = 'External tools'; $string['name'] = 'Name'; $string['never'] = 'Never'; $string['new_window'] = 'New window'; $string['no_lti_configured'] = 'There are no active external tools configured.'; $string['no_lti_pending'] = 'There are no pending external tools.'; $string['no_lti_rejected'] = 'There are no rejected external tools.'; $string['no_lti_tools'] = 'There are no external tools configured.'; $string['no_tp_accepted'] = 'There are no accepted external tool registrations.'; $string['no_tp_cancelled'] = 'There are no cancelled external tool registrations.'; $string['no_tp_configured'] = 'There are no unregistered external tool registrations configured.'; $string['no_tp_pending'] = 'There are no pending external tool registrations.'; $string['no_tp_rejected'] = 'There are no rejected external tool registrations.'; $string['noattempts'] = 'No attempts have been made on this tool instance'; $string['nocourseexternaltoolsnotice'] = 'There are no LTI External tools yet.'; $string['noltis'] = 'There are no external tool instances'; $string['noprofileservice'] = 'Profile service not found'; $string['noservers'] = 'No servers found'; $string['notypes'] = 'There are currently no LTI tools set up in Moodle. Click the Install link above to add some.'; $string['noviewusers'] = 'No users were found with permissions to use this tool'; $string['oauthsecurity'] = 'LTI 1.0/1.1'; $string['optionalsettings'] = 'Optional settings'; $string['organization'] = 'Organisation details'; $string['organizationdescr'] = 'Organisation description'; $string['organizationid_default'] = 'Default organisation ID'; $string['siteid'] = 'Site ID'; $string['sitehost'] = 'Site hostname'; $string['organizationid_default_help'] = 'The default value to use for Organisation ID. Site ID identifies this installation of Moodle.'; $string['organizationidguid'] = 'Organisation ID'; $string['organizationidguid_help'] = 'A unique identifier for this Moodle instance passed to the tool as the Platform Instance GUID. If this field is left blank, the default value will be used.'; $string['organizationurl'] = 'Organisation URL'; $string['organizationurl_help'] = 'The base URL of this Moodle instance. If this field is left blank, a default value will be used based on the site configuration.'; $string['pagesize'] = 'Submissions shown per page'; $string['parameter'] = 'Tool parameters'; $string['parameter_help'] = 'Tool parameters are settings requested to be passed by the tool provider in the accepted tool proxy.'; $string['password'] = 'Shared secret'; $string['password_admin'] = 'Shared secret'; $string['password_admin_help'] = 'The shared secret can be thought of as a password used to authenticate access to the tool. It should be provided along with the consumer key from the tool provider. Tools which do not require secure communication from Moodle and do not provide additional services (such as grade reporting) may not require a shared secret.'; $string['password_help'] = 'For pre-configured tools, it is not necessary to enter a shared secret here, as the shared secret will be provided as part of the configuration process. This field should be entered if creating a link to a tool provider which is not already configured. If the tool provider is to be used more than once in this course, adding a course tool configuration is a good idea. The shared secret can be thought of as a password used to authenticate access to the tool. It should be provided along with the consumer key from the tool provider. Tools which do not require secure communication from Moodle and do not provide additional services (such as grade reporting) may not require a shared secret.'; $string['pending'] = 'Pending'; $string['pluginadministration'] = 'External tool administration'; $string['pluginname'] = 'External tool'; $string['preferheight'] = 'Preferred height'; $string['preferwidget'] = 'Prefer widget launch'; $string['preferwidth'] = 'Preferred width'; $string['press_to_submit'] = 'Press to launch this activity'; $string['privacy'] = 'Privacy'; $string['privacy:metadata:courseid'] = 'The ID of the course the user is accessing the LTI Consumer from'; $string['privacy:metadata:courseidnumber'] = 'The ID number of the course the user is accessing the LTI Consumer from'; $string['privacy:metadata:coursefullname'] = 'The fullname of the course the user is accessing the LTI Consumer from'; $string['privacy:metadata:courseshortname'] = 'The shortname of the course the user is accessing the LTI Consumer from'; $string['privacy:metadata:createdby'] = 'The user who created the record'; $string['privacy:metadata:email'] = 'The email address of the user accessing the LTI Consumer'; $string['privacy:metadata:externalpurpose'] = 'The LTI Consumer provides user information and context to the LTI Tool Provider.'; $string['privacy:metadata:firstname'] = 'The firstname of the user accessing the LTI Consumer'; $string['privacy:metadata:fullname'] = 'The fullname of the user accessing the LTI Consumer'; $string['privacy:metadata:lastname'] = 'The lastname of the user accessing the LTI Consumer'; $string['privacy:metadata:lti_submission'] = 'LTI submission'; $string['privacy:metadata:lti_submission:datesubmitted'] = 'The timestamp indicating when the submission was made'; $string['privacy:metadata:lti_submission:dateupdated'] = 'The timestamp indicating when the submission was modified'; $string['privacy:metadata:lti_submission:gradepercent'] = 'The grade for the user as a percentage'; $string['privacy:metadata:lti_submission:originalgrade'] = 'The original grade for the user'; $string['privacy:metadata:lti_submission:userid'] = 'The ID of the user who submitted for the LTI activity'; $string['privacy:metadata:lti_tool_proxies'] = 'LTI proxies'; $string['privacy:metadata:lti_tool_proxies:name'] = 'LTI proxy name'; $string['privacy:metadata:lti_types'] = 'LTI types'; $string['privacy:metadata:lti_types:name'] = 'LTI type name'; $string['privacy:metadata:role'] = 'The role in the course for the user accessing the LTI Consumer'; $string['privacy:metadata:timecreated'] = 'The time when the record was created'; $string['privacy:metadata:timemodified'] = 'The time when the record was modified'; $string['privacy:metadata:userid'] = 'The ID of the user accessing the LTI Consumer'; $string['privacy:metadata:useridnumber'] = 'The ID number of the user accessing the LTI Consumer'; $string['privacy:metadata:username'] = 'The username of the user accessing the LTI Consumer'; $string['publickey'] = 'Public key'; $string['publickeyset'] = 'Public keyset'; $string['publickeyset_help'] = 'Public keyset from where this site will retrieve the tool\'s public key to allow signatures of incoming messages and service requests to be verified.'; $string['publickey_help'] = 'The public key (in PEM format) provided by the tool to allow signatures of incoming messages and service requests to be verified.'; $string['quickgrade'] = 'Allow quick grading'; $string['quickgrade_help'] = 'If enabled, multiple tools can be graded on one page. Add grades and comments then click the "Save all my feedback" button to save all changes for that page.'; $string['redirect'] = 'You will be redirected in few seconds. If you are not, press the button.'; $string['redirectionuris'] = 'Redirection URI(s)'; $string['redirectionuris_help'] = 'A list of URIs (one per line) which the tool uses when making authorisation requests. At least one must be registered before a message can be successfully sent to the tool.'; $string['register'] = 'Register'; $string['register_warning'] = 'The registration page seems to be taking a while to open. If it does not appear, check that you entered the correct URL in the configuration settings. If Moodle is using https, ensure the tool you are configuring supports https and you are using https in the URL.'; $string['registertype'] = 'Configure a new external tool registration'; $string['registration_options'] = 'Registration options'; $string['registrationname'] = 'Tool provider name'; $string['registrationname_help'] = 'Enter the name of the tool provider being registered.'; $string['registrationurl'] = 'Registration URL'; $string['registrationurl_help'] = 'The registration URL should be available from the tool provider as the location to which registration requests should be sent.'; $string['reject'] = 'Reject'; $string['rejected'] = 'Rejected'; $string['resource'] = 'Resource'; $string['resourcekey'] = 'Consumer key'; $string['resourcekey_admin'] = 'Consumer key'; $string['resourcekey_admin_help'] = 'The consumer key can be thought of as a username used to authenticate access to the tool. It can be used by the tool provider to uniquely identify the Moodle site from which users launch into the tool. The consumer key must be provided by the tool provider. The method of obtaining a consumer key varies between tool providers. It may be an automated process, or it may require a dialogue with the tool provider. Tools which do not require secure communication from Moodle and do not provide additional services (such as grade reporting) may not require a resource key.'; $string['resourcekey_help'] = 'For pre-configured tools, it is not necessary to enter a resource key here, as the consumer key will be provided as part of the configuration process. This field should be entered if creating a link to a tool provider which is not already configured. If the tool provider is to be used more than once in this course, adding a course tool configuration is a good idea. The consumer key can be thought of as a username used to authenticate access to the tool. It can be used by the tool provider to uniquely identify the Moodle site from which users launch into the tool. The consumer key must be provided by the tool provider. The method of obtaining a consumer key varies between tool providers. It may be an automated process, or it may require a dialogue with the tool provider. Tools which do not require secure communication from Moodle and do not provide additional services (such as grade reporting) may not require a resource key.'; $string['resourceurl'] = 'Resource URL'; $string['restricttocategory'] = 'Restrict to category'; $string['restricttocategory_help'] = 'To restrict use of this tool to courses within a category, select the category or categories from the list.'; $string['return_to_course'] = 'Click here to return to the course.'; $string['saveallfeedback'] = 'Save all my feedback'; $string['search:activity'] = 'External tool - activity information'; $string['secure_icon_url'] = 'Secure icon URL'; $string['secure_icon_url_help'] = 'Similar to the icon URL, but used when the site is accessed securely through SSL. This field is to prevent the browser from displaying a warning about an insecure image.'; $string['secure_launch_url'] = 'Secure tool URL'; $string['secure_launch_url_help'] = 'Similar to the tool URL, but used instead of the tool URL if high security is required. Moodle will use the secure tool URL instead of the tool URL if the Moodle site is accessed through SSL, or if the tool configuration is set to always launch through SSL. The tool URL may also be set to an https address to force launching through SSL, and this field may be left blank.'; $string['selectcontent'] = 'Select content'; $string['send'] = 'Send'; $string['services'] = 'Services'; $string['services_help'] = 'Select those services which you wish to offer to the tool provider. More than one service can be selected.'; $string['setupoptions'] = 'Set-up options'; $string['share_email'] = 'Share launcher\'s email with the tool'; $string['share_email_admin'] = 'Share launcher\'s email with tool'; $string['share_email_admin_help'] = 'Specify whether the e-mail address of the user launching the tool will be shared with the tool provider. The tool provider may need launcher\'s e-mail addresses to distinguish users with the same name in the UI, or send e-mails to users based on actions within the tool.'; $string['share_email_help'] = 'Specify whether the e-mail address of the user launching the tool will be shared with the tool provider. The tool provider may need launcher\'s email addresses to distinguish users with the same name, or send emails to users based on actions within the tool. Note that this setting may be overridden in the tool configuration.'; $string['share_name'] = 'Share launcher\'s name with the tool'; $string['share_name_admin'] = 'Share launcher\'s name with tool'; $string['share_name_admin_help'] = 'Specify whether the full name of the user launching the tool should be shared with the tool provider. The tool provider may need launchers\' names to show meaningful information within the tool.'; $string['share_name_help'] = 'Specify whether the full name of the user launching the tool should be shared with the tool provider. The tool provider may need launchers\' names to show meaningful information within the tool. Note that this setting may be overridden in the tool configuration.'; $string['share_roster'] = 'Allow the tool to access this course\'s roster'; $string['share_roster_admin'] = 'Tool may access course roster'; $string['share_roster_admin_help'] = 'Specify whether the tool can access the list of users enrolled in courses from which this tool is launched.'; $string['share_roster_help'] = 'Specify whether the tool can access the list of users enrolled in this course. Note that this setting may be overridden in the tool configuration.'; $string['show_in_course_activity_chooser'] = 'Show in activity chooser and as a preconfigured tool'; $string['showinactivitychooser'] = 'Show in activity chooser'; $string['dontshowinactivitychooser'] = 'Don\'t show in activity chooser'; $string['show_in_course_lti1'] = 'Tool configuration usage'; $string['show_in_course_lti1_help'] = 'This tool may be shown in the activity chooser for a teacher to select to add to a course. Alternatively, it may be shown in the preconfigured tool drop-down menu when adding an external tool to a course. A further option is for the tool configuration to only be used if the exact tool URL is entered when adding an external tool to a course.'; $string['show_in_course_lti2'] = 'Tool configuration usage'; $string['show_in_course_lti2_help'] = 'This tool can be shown in the activity chooser for a teacher to select to add to a course or in the preconfigured tool drop-down menu when adding an external tool to a course.'; $string['show_in_course_no'] = 'Do not show; use only when a matching tool URL is entered'; $string['show_in_course_preconfigured'] = 'Show as preconfigured tool when adding an external tool'; $string['size'] = 'Size parameters'; $string['opensslconfiginvalid'] = 'LTI 1.3 requires a valid openssl.cnf to be configured and available to your web server. Please contact the site administrator to configure and enable openssl for this site.'; $string['submission'] = 'Submission'; $string['submissions'] = 'Submissions'; $string['submissionsfor'] = 'Submissions for {$a}'; $string['successfullycreatedtooltype'] = 'Successfully created new tool!'; $string['successfullyfetchedtoolconfigurationfromcontent'] = 'Successfully fetched tool configuration from the selected content.'; $string['subplugintype_ltiresource'] = 'LTI service resource'; $string['subplugintype_ltiresource_plural'] = 'LTI service resources'; $string['subplugintype_ltiservice'] = 'LTI service'; $string['subplugintype_ltiservice_plural'] = 'LTI services'; $string['subplugintype_ltisource'] = 'LTI source'; $string['subplugintype_ltisource_plural'] = 'LTI sources'; $string['toggle_debug_data'] = 'Toggle debug data'; $string['tool_settings'] = 'Tool settings'; $string['tooldescription'] = 'Tool description'; $string['tooldescription_help'] = 'The description of the tool that will be displayed to teachers in the activity list. This should describe what the tool is for and what it does and any additional information the teacher may need to know.'; $string['tooldetailsaccesstokenurl'] = 'Access token URL'; $string['tooldetailsauthrequesturl'] = 'Authentication request URL'; $string['tooldetailsclientid'] = 'Client ID'; $string['tooldetailsdeploymentid'] = 'Deployment ID'; $string['tooldetailsmailtosubject'] = 'LTI tool configuration'; $string['tooldetailsmodalemail'] = 'Email'; $string['tooldetailsmodallink'] = 'View configuration details'; $string['tooldetailsmodaltitle'] = 'Tool configuration details'; $string['tooldetailsplatformid'] = 'Platform ID'; $string['tooldetailspublickeyseturl'] = 'Public keyset URL'; $string['toolisbeingused'] = 'This tool is being used {$a} times'; $string['toolisnotbeingused'] = 'This tool has not yet been used'; $string['toolproxy'] = 'External tool registrations'; $string['toolproxy_help'] = 'External tool registrations allow Moodle site administrators to configure external tools from a tool proxy obtained from a tool provider supporting LTI 2.0. A registration URL provided by the tool provider is all that is required to initiate the process. The capabilities and services offered to the tool provider are selected when configuring a new registration. Tool registrations listed on this page are separated into four categories: * **Configured** - These tool registrations have been set up but the registration process has not yet been started. * **Pending** - The registration process for these tool registrations has been started but has not completed. Open and save the settings to move it back to the \'Configured\' category. * **Accepted** - These tool registrations have been approved; the resources specified in the tool proxy will appear on the preconfigured tools page with an initial status of \'Pending\'. * **Rejected** - These tool registrations are ones which were rejected during the registration process. Open and save the settings to move it back to the \'Configured\' category so the registration process can be restarted.'; $string['toolproxyregistration'] = 'External tool registration'; $string['toolregistration'] = 'External tool registration'; $string['toolsetup'] = 'External tool configuration'; $string['tooltypenotfounderror'] = "The LTI tool used in this activity has been deleted. If you need help, contact your teacher or site administrator."; $string['tooltypes'] = 'Tools'; $string['toolurl'] = 'Tool URL'; $string['toolurlplaceholder'] = 'Tool URL...'; $string['toolurl_help'] = 'The tool URL is used to match tool URLs to the correct tool configuration. Prefixing the URL with http(s) is optional. Additionally, the base URL is used as the tool URL if a tool URL is not specified in the external tool instance. For example, a base URL of *tool.com* would match the following: * tool.com * tool.com/quizzes * tool.com/quizzes/quiz.php?id=10 * www.tool.com/quizzes A base URL of *www.tool.com/quizzes* would match the following: * www.tool.com/quizzes * tool.com/quizzes * tool.com/quizzes/take.php?id=10 A base URL of *quiz.tool.com* would match the following: * quiz.tool.com * quiz.tool.com/take.php?id=10 If two different tool configurations are for the same domain, the most specific match will be used. You can also insert a cartridge URL if you have one and the details for the tool will be automatically filled.'; $string['toolurl_contentitemselectionrequest'] = 'Content Selection URL'; $string['toolurl_contentitemselectionrequest_help'] = 'The Content Selection URL will be used to launch the content selection page from the tool provider. If it is empty, the Tool URL will be used'; $string['typename'] = 'Tool name'; $string['typename_help'] = 'The tool name is used to identify the tool provider within Moodle. The name entered will be visible to teachers when adding external tools within courses.'; $string['types'] = 'Types'; $string['unabletocreatetooltype'] = 'Unable to create tool'; $string['unabletofindtooltype'] = 'Unable to find tool for {$a->id}'; $string['unknownstate'] = 'Unknown state'; $string['update'] = 'Update'; $string['usage'] = 'Usage count'; $string['useraccountinformation'] = 'User account information'; $string['userpersonalinformation'] = 'User personal information'; $string['validurl'] = 'A valid URL must start with http(s)://'; $string['viewsubmissions'] = 'View submissions and grading screen'; // Deprecated since Moodle 4.5. $string['selectcontentvalidationerror'] = 'You need to select content for this activity.'; lang/en/deprecated.txt000064400000000045152265520610010733 0ustar00selectcontentvalidationerror,mod_lti toolconfigure.php000064400000004102152265520610010135 0ustar00. /** * This page allows the configuration of external tools that meet the LTI specification. * * @package mod_lti * @copyright 2015 Ryan Wyllie * @author Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $cartridgeurl = optional_param('cartridgeurl', '', PARAM_URL); // No guest autologin. require_login(0, false); admin_externalpage_setup('ltitoolconfigure'); if ($cartridgeurl) { $type = new stdClass(); $data = new stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $data->lti_coursevisible = 1; lti_load_type_from_cartridge($cartridgeurl, $data); lti_add_type($type, $data); } $pageurl = new moodle_url('/mod/lti/toolconfigure.php'); $PAGE->set_url($pageurl); $PAGE->set_title(get_string('toolregistration', 'mod_lti')); $PAGE->requires->string_for_js('success', 'moodle'); $PAGE->requires->string_for_js('error', 'moodle'); $PAGE->requires->string_for_js('successfullycreatedtooltype', 'mod_lti'); $PAGE->requires->string_for_js('failedtocreatetooltype', 'mod_lti'); $output = $PAGE->get_renderer('mod_lti'); echo $output->header(); $page = new \mod_lti\output\tool_configure_page(); echo $output->render($page); echo $output->footer(); grade.php000064400000002555152265520610006352 0ustar00. /** * This page is the grade hook allowing LTI launches from gradebook. * * @package mod_lti * @category grade * @copyright 2022 Cengage Group {@link https://cengage.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); $id = required_param('id', PARAM_INT); $userid = optional_param('userid', 0, PARAM_INT); $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); require_login($course, false, $cm); redirect(new moodle_url('/mod/lti/view.php', array( 'id' => $cm->id, 'action' => 'gradeReport', 'user' => $userid))); servicelib.php000064400000023617152265520610007421 0ustar00. /** * Utility code for LTI service handling. * * @package mod_lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Chris Scribner */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/OAuthBody.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; define('LTI_ITEM_TYPE', 'mod'); define('LTI_ITEM_MODULE', 'lti'); define('LTI_SOURCE', 'mod/lti'); function lti_get_response_xml($codemajor, $description, $messageref, $messagetype) { $xml = new SimpleXMLElement(''); $xml->addAttribute('xmlns', 'http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0'); $headerinfo = $xml->addChild('imsx_POXHeader')->addChild('imsx_POXResponseHeaderInfo'); $headerinfo->addChild('imsx_version', 'V1.0'); $headerinfo->addChild('imsx_messageIdentifier', (string)mt_rand()); $statusinfo = $headerinfo->addChild('imsx_statusInfo'); $statusinfo->addchild('imsx_codeMajor', $codemajor); $statusinfo->addChild('imsx_severity', 'status'); $statusinfo->addChild('imsx_description', $description); $statusinfo->addChild('imsx_messageRefIdentifier', $messageref); $incomingtype = str_replace('Response', 'Request', $messagetype); $statusinfo->addChild('imsx_operationRefIdentifier', $incomingtype); $xml->addChild('imsx_POXBody')->addChild($messagetype); return $xml; } function lti_parse_message_id($xml) { if (empty($xml->imsx_POXHeader)) { return ''; } $node = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier; $messageid = (string)$node; return $messageid; } function lti_parse_grade_replace_message($xml) { $node = $xml->imsx_POXBody->replaceResultRequest->resultRecord->sourcedGUID->sourcedId; $resultjson = json_decode((string)$node); if ( is_null($resultjson) ) { throw new Exception('Invalid sourcedId in result message'); } $node = $xml->imsx_POXBody->replaceResultRequest->resultRecord->result->resultScore->textString; $score = (string) $node; if ( ! is_numeric($score) ) { throw new Exception('Score must be numeric'); } $grade = floatval($score); if ( $grade < 0.0 || $grade > 1.0 ) { throw new Exception('Score not between 0.0 and 1.0'); } $parsed = new stdClass(); $parsed->gradeval = $grade; $parsed->instanceid = $resultjson->data->instanceid; $parsed->userid = $resultjson->data->userid; $parsed->launchid = $resultjson->data->launchid; $parsed->typeid = $resultjson->data->typeid; $parsed->sourcedidhash = $resultjson->hash; $parsed->messageid = lti_parse_message_id($xml); return $parsed; } function lti_parse_grade_read_message($xml) { $node = $xml->imsx_POXBody->readResultRequest->resultRecord->sourcedGUID->sourcedId; $resultjson = json_decode((string)$node); if ( is_null($resultjson) ) { throw new Exception('Invalid sourcedId in result message'); } $parsed = new stdClass(); $parsed->instanceid = $resultjson->data->instanceid; $parsed->userid = $resultjson->data->userid; $parsed->launchid = $resultjson->data->launchid; $parsed->typeid = $resultjson->data->typeid; $parsed->sourcedidhash = $resultjson->hash; $parsed->messageid = lti_parse_message_id($xml); return $parsed; } function lti_parse_grade_delete_message($xml) { $node = $xml->imsx_POXBody->deleteResultRequest->resultRecord->sourcedGUID->sourcedId; $resultjson = json_decode((string)$node); if ( is_null($resultjson) ) { throw new Exception('Invalid sourcedId in result message'); } $parsed = new stdClass(); $parsed->instanceid = $resultjson->data->instanceid; $parsed->userid = $resultjson->data->userid; $parsed->launchid = $resultjson->data->launchid; $parsed->typeid = $resultjson->data->typeid; $parsed->sourcedidhash = $resultjson->hash; $parsed->messageid = lti_parse_message_id($xml); return $parsed; } function lti_accepts_grades($ltiinstance) { global $DB; $acceptsgrades = true; $ltitype = $DB->get_record('lti_types', array('id' => $ltiinstance->typeid)); if (empty($ltitype->toolproxyid)) { $typeconfig = lti_get_config($ltiinstance); $typeacceptgrades = isset($typeconfig['acceptgrades']) ? $typeconfig['acceptgrades'] : LTI_SETTING_DELEGATE; if (!($typeacceptgrades == LTI_SETTING_ALWAYS || ($typeacceptgrades == LTI_SETTING_DELEGATE && $ltiinstance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) { $acceptsgrades = false; } } else { $enabledcapabilities = explode("\n", $ltitype->enabledcapability); $acceptsgrades = in_array('Result.autocreate', $enabledcapabilities) || in_array('BasicOutcome.url', $enabledcapabilities); } return $acceptsgrades; } /** * Set the passed user ID to the session user. * * @param int $userid */ function lti_set_session_user($userid) { global $DB; if ($user = $DB->get_record('user', array('id' => $userid))) { \core\session\manager::set_user($user); } } function lti_update_grade($ltiinstance, $userid, $launchid, $gradeval) { global $CFG, $DB; require_once($CFG->libdir . '/gradelib.php'); $params = array(); $params['itemname'] = $ltiinstance->name; $gradeval = $gradeval * floatval($ltiinstance->grade); $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = $gradeval; $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade, $params); $record = $DB->get_record('lti_submission', array('ltiid' => $ltiinstance->id, 'userid' => $userid, 'launchid' => $launchid), 'id'); if ($record) { $id = $record->id; } else { $id = null; } if (!empty($id)) { $DB->update_record('lti_submission', array( 'id' => $id, 'dateupdated' => time(), 'gradepercent' => $gradeval, 'state' => 2 )); } else { $DB->insert_record('lti_submission', array( 'ltiid' => $ltiinstance->id, 'userid' => $userid, 'datesubmitted' => time(), 'dateupdated' => time(), 'gradepercent' => $gradeval, 'originalgrade' => $gradeval, 'launchid' => $launchid, 'state' => 1 )); } return $status == GRADE_UPDATE_OK; } function lti_read_grade($ltiinstance, $userid) { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $grades = grade_get_grades($ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, $userid); $ltigrade = floatval($ltiinstance->grade); if (!empty($ltigrade) && isset($grades) && isset($grades->items[0]) && is_array($grades->items[0]->grades)) { foreach ($grades->items[0]->grades as $agrade) { $grade = $agrade->grade; if (isset($grade)) { return $grade / $ltigrade; } } } } function lti_delete_grade($ltiinstance, $userid) { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = null; $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade); return $status == GRADE_UPDATE_OK; } function lti_verify_message($key, $sharedsecrets, $body, $headers = null) { foreach ($sharedsecrets as $secret) { $signaturefailed = false; try { // TODO: Switch to core oauthlib once implemented - MDL-30149. lti\handle_oauth_body_post($key, $secret, $body, $headers); } catch (Exception $e) { debugging('LTI message verification failed: '.$e->getMessage()); $signaturefailed = true; } if (!$signaturefailed) { return $secret; // Return the secret used to sign the message). } } return false; } /** * Validate source ID from external request * * @param object $ltiinstance * @param object $parsed * @throws Exception */ function lti_verify_sourcedid($ltiinstance, $parsed) { $sourceid = lti_build_sourcedid($parsed->instanceid, $parsed->userid, $ltiinstance->servicesalt, $parsed->typeid, $parsed->launchid); if ($sourceid->hash != $parsed->sourcedidhash) { throw new Exception('SourcedId hash not valid'); } } /** * Extend the LTI services through the ltisource plugins * * @param stdClass $data LTI request data * @return bool * @throws coding_exception */ function lti_extend_lti_services($data) { $plugins = get_plugin_list_with_function('ltisource', $data->messagetype); if (!empty($plugins)) { // There can only be one. if (count($plugins) > 1) { throw new coding_exception('More than one ltisource plugin handler found'); } $data->xml = new SimpleXMLElement($data->body); $callback = current($plugins); call_user_func($callback, $data); return true; } return false; } backup/moodle2/backup_lti_activity_task.class.php000064400000007235152265520610016255 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * Defines backup_lti_activity_task class * * @package mod_lti * @category backup * @copyright 2009 Marc Alier , Jordi Piguillem, Nikolas Galanis * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/mod/lti/backup/moodle2/backup_lti_stepslib.php'); /** * Provides the steps to perform one complete backup of the LTI instance */ class backup_lti_activity_task extends backup_activity_task { /** * No specific settings for this activity */ protected function define_my_settings() { } /** * Defines a backup step to store the instance data in the lti.xml file */ protected function define_my_steps() { $this->add_step(new backup_lti_activity_structure_step('lti_structure', 'lti.xml')); } /** * Encodes URLs to the index.php and view.php scripts * * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot, "/"); // Link to the list of basiclti tools. $search = "/(".$base."\/mod\/lti\/index.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@LTIINDEX*$2@$', $content); // Link to basiclti view by moduleid. $search = "/(".$base."\/mod\/lti\/view.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@LTIVIEWBYID*$2@$', $content); return $content; } } backup/moodle2/backup_lti_stepslib.php000064400000024202152265520610014111 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains all the backup steps that will be used * by the backup_lti_activity_task * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Define the complete assignment structure for backup, with file and id annotations */ class backup_lti_activity_structure_step extends backup_activity_structure_step { /** * Defines structure of activity backup * @return backup_nested_element */ protected function define_structure() { global $DB; // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); // Define each element separated. $lti = new backup_nested_element('lti', array('id'), array( 'name', 'intro', 'introformat', 'timecreated', 'timemodified', 'typeid', 'toolurl', 'securetoolurl', 'preferheight', 'launchcontainer', 'instructorchoicesendname', 'instructorchoicesendemailaddr', 'instructorchoiceacceptgrades', 'instructorchoiceallowroster', 'instructorchoiceallowsetting', 'grade', 'instructorcustomparameters', 'debuglaunch', 'showtitlelaunch', 'showdescriptionlaunch', 'icon', 'secureicon', new encrypted_final_element('resourcekey'), new encrypted_final_element('password'), ) ); $ltitype = new backup_nested_element('ltitype', array('id'), array( 'name', 'baseurl', 'tooldomain', 'state', 'course', 'coursevisible', 'ltiversion', 'clientid', 'toolproxyid', 'enabledcapability', 'parameter', 'icon', 'secureicon', 'createdby', 'timecreated', 'timemodified', 'description' ) ); $ltitypesconfigs = new backup_nested_element('ltitypesconfigs'); $ltitypesconfig = new backup_nested_element('ltitypesconfig', array('id'), array( 'name', 'value', ) ); $ltitypesconfigencrypted = new backup_nested_element('ltitypesconfigencrypted', array('id'), array( 'name', new encrypted_final_element('value'), ) ); $ltitoolproxy = new backup_nested_element('ltitoolproxy', array('id')); $ltitoolsettings = new backup_nested_element('ltitoolsettings'); $ltitoolsetting = new backup_nested_element('ltitoolsetting', array('id'), array( 'settings', 'timecreated', 'timemodified', ) ); $ltisubmissions = new backup_nested_element('ltisubmissions'); $ltisubmission = new backup_nested_element('ltisubmission', array('id'), array( 'userid', 'datesubmitted', 'dateupdated', 'gradepercent', 'originalgrade', 'launchid', 'state' )); $lticoursevisible = new backup_nested_element('lticoursevisible', ['id'], [ 'typeid', 'courseid', 'coursevisible', ]); // Build the tree $lti->add_child($ltitype); $ltitype->add_child($ltitypesconfigs); $ltitypesconfigs->add_child($ltitypesconfig); $ltitypesconfigs->add_child($ltitypesconfigencrypted); $ltitype->add_child($ltitoolproxy); $ltitoolproxy->add_child($ltitoolsettings); $ltitoolsettings->add_child($ltitoolsetting); $lti->add_child($ltisubmissions); $ltisubmissions->add_child($ltisubmission); $lti->add_child($lticoursevisible); // Define sources. $ltirecord = $DB->get_record('lti', ['id' => $this->task->get_activityid()]); $lti->set_source_array([$ltirecord]); $ltitypedata = $this->retrieve_lti_type($ltirecord); $ltitype->set_source_array($ltitypedata ? [$ltitypedata] : []); if (isset($ltitypedata->baseurl)) { // Add type config values only if the type was backed up. Encrypt password and resourcekey. $params = [backup_helper::is_sqlparam($ltitypedata->id), backup_helper::is_sqlparam('password'), backup_helper::is_sqlparam('resourcekey')]; $ltitypesconfig->set_source_sql("SELECT id, name, value FROM {lti_types_config} WHERE typeid = ? AND name <> ? AND name <> ?", $params); $ltitypesconfigencrypted->set_source_sql("SELECT id, name, value FROM {lti_types_config} WHERE typeid = ? AND (name = ? OR name = ?)", $params); } if (!empty($ltitypedata->toolproxyid)) { // If this is LTI 2 tool add settings for the current activity. $ltitoolproxy->set_source_array([['id' => $ltitypedata->toolproxyid]]); $ltitoolsetting->set_source_sql("SELECT * FROM {lti_tool_settings} WHERE toolproxyid = ? AND course = ? AND coursemoduleid = ?", [backup_helper::is_sqlparam($ltitypedata->toolproxyid), backup::VAR_COURSEID, backup::VAR_MODID]); } else { $ltitoolproxy->set_source_array([]); } // All the rest of elements only happen if we are including user info. if ($userinfo) { $ltisubmission->set_source_table('lti_submission', array('ltiid' => backup::VAR_ACTIVITYID)); } $lticoursevisibledata = $this->retrieve_lti_coursevisible($ltirecord); $lticoursevisible->set_source_array($lticoursevisibledata ? [$lticoursevisibledata] : []); // Define id annotations $ltitype->annotate_ids('user', 'createdby'); $ltitype->annotate_ids('course', 'course'); $ltisubmission->annotate_ids('user', 'userid'); // Define file annotations. $lti->annotate_files('mod_lti', 'intro', null); // This file areas haven't itemid. // Add support for subplugin structures. $this->add_subplugin_structure('ltisource', $lti, true); $this->add_subplugin_structure('ltiservice', $lti, true); // Return the root element (lti), wrapped into standard activity structure. return $this->prepare_activity_structure($lti); } /** * Retrieves a record from {lti_type} table associated with the current activity * * Information about site tools is not returned because it is insecure to back it up, * only fields necessary for same-site tool matching are left in the record * * @param stdClass $ltirecord record from {lti} table * @return stdClass|null */ protected function retrieve_lti_type($ltirecord) { global $DB; if (!$ltirecord->typeid) { return null; } $record = $DB->get_record('lti_types', ['id' => $ltirecord->typeid]); if ($record && $record->course == SITEID) { // Site LTI types or registrations are not backed up except for their name (which is visible). // Predefined course types can be backed up. $allowedkeys = ['id', 'course', 'name', 'toolproxyid']; foreach ($record as $key => $value) { if (!in_array($key, $allowedkeys)) { $record->$key = null; } } } return $record; } /** * Retrieves a record from {lti_coursevisible} table associated with the current type * * @param stdClass $ltirecord record from {lti} table * @return mixed */ protected function retrieve_lti_coursevisible(stdClass $ltirecord): mixed { global $DB; if (!$ltirecord->typeid) { return null; } return $DB->get_record('lti_coursevisible', ['typeid' => $ltirecord->typeid, 'courseid' => $ltirecord->course]); } } backup/moodle2/restore_lti_activity_task.class.php000064400000011754152265520610016474 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains the lti module restore class * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/lti/backup/moodle2/restore_lti_stepslib.php'); /** * basiclti restore task that provides all the settings and steps to perform one * complete restore of the activity */ class restore_lti_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity. } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { // Label only has one structure step. $this->add_step(new restore_lti_activity_structure_step('lti_structure', 'lti.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('lti', array('intro'), 'lti'); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { $rules = array(); $rules[] = new restore_decode_rule('LTIVIEWBYID', '/mod/lti/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('LTIINDEX', '/mod/lti/index.php?id=$1', 'course'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * basiclti logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('lti', 'add', 'view.php?id={course_module}', '{lti}'); $rules[] = new restore_log_rule('lti', 'update', 'view.php?id={course_module}', '{lti}'); $rules[] = new restore_log_rule('lti', 'view', 'view.php?id={course_module}', '{lti}'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('lti', 'view all', 'index.php?id={course}', null); return $rules; } } backup/moodle2/restore_lti_stepslib.php000064400000027261152265520610014337 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains all the restore steps that will be used * by the restore_lti_activity_task * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; use mod_lti\local\ltiopenid\registration_helper; /** * Structure step to restore one lti activity */ class restore_lti_activity_structure_step extends restore_activity_structure_step { /** @var bool */ protected $newltitype = false; protected function define_structure() { $paths = array(); // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); $lti = new restore_path_element('lti', '/activity/lti'); $paths[] = $lti; $paths[] = new restore_path_element('ltitype', '/activity/lti/ltitype'); $paths[] = new restore_path_element('ltitypesconfig', '/activity/lti/ltitype/ltitypesconfigs/ltitypesconfig'); $paths[] = new restore_path_element('ltitypesconfigencrypted', '/activity/lti/ltitype/ltitypesconfigs/ltitypesconfigencrypted'); $paths[] = new restore_path_element('ltitoolproxy', '/activity/lti/ltitype/ltitoolproxy'); $paths[] = new restore_path_element('ltitoolsetting', '/activity/lti/ltitype/ltitoolproxy/ltitoolsettings/ltitoolsetting'); if ($userinfo) { $submission = new restore_path_element('ltisubmission', '/activity/lti/ltisubmissions/ltisubmission'); $paths[] = $submission; } $paths[] = new restore_path_element('lticoursevisible', '/activity/lti/lticoursevisible'); // Add support for subplugin structures. $this->add_subplugin_structure('ltisource', $lti); $this->add_subplugin_structure('ltiservice', $lti); // Return the paths wrapped into standard activity structure. return $this->prepare_activity_structure($paths); } protected function process_lti($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); $data->servicesalt = uniqid('', true); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. // Grade used to be a float (whole numbers only), restore as int. $data->grade = (int) $data->grade; $data->typeid = 0; // Try to decrypt resourcekey and password. Null if not possible (DB default). // Note these fields were originally encrypted on backup using {link @encrypted_final_element}. $data->resourcekey = isset($data->resourcekey) ? $this->decrypt($data->resourcekey) : null; $data->password = isset($data->password) ? $this->decrypt($data->password) : null; $newitemid = $DB->insert_record('lti', $data); // Immediately after inserting "activity" record, call this. $this->apply_activity_instance($newitemid); } /** * Process an lti type restore * @param mixed $data The data from backup XML file * @return void */ protected function process_ltitype($data) { global $DB, $USER; $data = (object)$data; $oldid = $data->id; if (!empty($data->createdby)) { $data->createdby = $this->get_mappingid('user', $data->createdby) ?: $USER->id; } $courseid = $this->get_courseid(); $data->course = ($this->get_mappingid('course', $data->course) == $courseid) ? $courseid : SITEID; // Try to find existing lti type with the same properties. $ltitypeid = $this->find_existing_lti_type($data); $this->newltitype = false; if (!$ltitypeid && $data->course == $courseid) { unset($data->toolproxyid); // Course tools can not use LTI2. if (!empty($data->clientid)) { // Need to rebuild clientid to ensure uniqueness. $data->clientid = registration_helper::get()->new_clientid(); } $ltitypeid = $DB->insert_record('lti_types', $data); $this->newltitype = true; $this->set_mapping('ltitype', $oldid, $ltitypeid); } // Add the typeid entry back to LTI module. $DB->update_record('lti', ['id' => $this->get_new_parentid('lti'), 'typeid' => $ltitypeid]); } /** * Process an lti coursevisible restore * @param mixed $data The data from backup XML file * @return void */ protected function process_lticoursevisible($data) { global $DB; $data = (object)$data; $data->typeid = $this->get_new_parentid('ltitype'); $data->courseid = $this->get_courseid(); if ($data->typeid) { $DB->insert_record('lti_coursevisible', $data); } } /** * Attempts to find existing record in lti_type * @param stdClass $data * @return int|null field lti_types.id or null if tool is not found */ protected function find_existing_lti_type($data) { global $DB; if ($ltitypeid = $this->get_mappingid('ltitype', $data->id)) { return $ltitypeid; } $ltitype = null; $params = (array)$data; if ($this->task->is_samesite()) { // If we are restoring on the same site try to find lti type with the same id. $sql = 'id = :id AND course = :course'; $sql .= ($data->toolproxyid) ? ' AND toolproxyid = :toolproxyid' : ' AND toolproxyid IS NULL'; if ($DB->record_exists_select('lti_types', $sql, $params)) { $this->set_mapping('ltitype', $data->id, $data->id); if ($data->toolproxyid) { $this->set_mapping('ltitoolproxy', $data->toolproxyid, $data->toolproxyid); } return $data->id; } } if ($data->course != $this->get_courseid()) { // Site tools are not backed up and are not restored. return null; } // Now try to find the same type on the current site available in this course. // Compare only fields baseurl, course and name, if they are the same we assume it is the same tool. // LTI2 is not possible in the course so we add "lt.toolproxyid IS NULL" to the query. $sql = 'SELECT id FROM {lti_types} WHERE ' . $DB->sql_compare_text('baseurl', 255) . ' = ' . $DB->sql_compare_text(':baseurl', 255) . ' AND course = :course AND name = :name AND toolproxyid IS NULL'; if ($ltitype = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE)) { $this->set_mapping('ltitype', $data->id, $ltitype->id); return $ltitype->id; } return null; } /** * Process an lti config restore * @param mixed $data The data from backup XML file */ protected function process_ltitypesconfig($data) { global $DB; $data = (object)$data; $data->typeid = $this->get_new_parentid('ltitype'); // Only add configuration if the new lti_type was created. if ($data->typeid && $this->newltitype) { if ($data->name == 'servicesalt') { $data->value = uniqid('', true); } $DB->insert_record('lti_types_config', $data); } } /** * Process an lti config restore * @param mixed $data The data from backup XML file */ protected function process_ltitypesconfigencrypted($data) { global $DB; $data = (object)$data; $data->typeid = $this->get_new_parentid('ltitype'); // Only add configuration if the new lti_type was created. if ($data->typeid && $this->newltitype) { $data->value = $this->decrypt($data->value); if (!is_null($data->value)) { $DB->insert_record('lti_types_config', $data); } } } /** * Process a restore of LTI tool registration * This method is empty because we actually process registration as part of process_ltitype() * @param mixed $data The data from backup XML file */ protected function process_ltitoolproxy($data) { } /** * Process an lti tool registration settings restore (only settings for the current activity) * @param mixed $data The data from backup XML file */ protected function process_ltitoolsetting($data) { global $DB; $data = (object)$data; $data->toolproxyid = $this->get_new_parentid('ltitoolproxy'); if (!$data->toolproxyid) { return; } $data->course = $this->get_courseid(); $data->coursemoduleid = $this->task->get_moduleid(); $DB->insert_record('lti_tool_settings', $data); } /** * Process a submission restore * @param mixed $data The data from backup XML file */ protected function process_ltisubmission($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->ltiid = $this->get_new_parentid('lti'); $data->datesubmitted = $this->apply_date_offset($data->datesubmitted); $data->dateupdated = $this->apply_date_offset($data->dateupdated); if ($data->userid > 0) { $data->userid = $this->get_mappingid('user', $data->userid); } $newitemid = $DB->insert_record('lti_submission', $data); $this->set_mapping('ltisubmission', $oldid, $newitemid); } protected function after_execute() { // Add lti related files, no need to match by itemname (just internally handled context). $this->add_related_files('mod_lti', 'intro', null); } } backup/moodle1/lib.php000064400000012325152265520610010637 0ustar00. /** * 1.9 to 2.0 backup format converter. (Also currently used in common cartridge import process) * * @package mod_lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Darko Miletic */ defined('MOODLE_INTERNAL') || die(); class moodle1_mod_lti_handler extends moodle1_mod_handler { /** @var moodle1_file_manager */ protected $fileman = null; /** @var int cmid */ protected $moduleid = null; /** * Declare the paths in moodle.xml we are able to convert * * The method returns list of {@link convert_path} instances. * For each path returned, the corresponding conversion method must be * defined. * * Note that the path /MOODLE_BACKUP/COURSE/MODULES/MOD/LTI does not * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * * @return array of {@link convert_path} instances */ public function get_paths() { return array( new convert_path( 'basiclti', '/MOODLE_BACKUP/COURSE/MODULES/MOD/LTI' ) ); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/LTI * data available */ public function process_basiclti($data) { global $DB; // Get the course module id and context id. $instanceid = $data['id']; $cminfo = $this->get_cminfo($instanceid); $this->moduleid = $cminfo['id']; $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid); // Get a fresh new file manager for this instance. $this->fileman = $this->converter->get_file_manager($contextid, 'mod_lti'); // Convert course files embedded into the intro. $this->fileman->filearea = 'intro'; $this->fileman->itemid = 0; $data['intro'] = moodle1_converter::migrate_referenced_files($data['intro'], $this->fileman); // Start writing assignment.xml. $this->open_xml_writer("activities/lti_{$this->moduleid}/lti.xml"); $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $this->moduleid, 'modulename' => 'lti', 'contextid' => $contextid)); $this->xmlwriter->begin_tag('lti', array('id' => $instanceid)); $ignorefields = array('id', 'modtype'); if (!$DB->record_exists('lti_types', array('id' => $data['typeid']))) { $ntypeid = false; $toolurls = $DB->get_records_select( 'lti_types_config', "name = 'toolurl' AND " . $DB->sql_compare_text('value', 256) . ' = ' . $DB->sql_compare_text('?', 256), [$data['toolurl']], '', 'id, value' ); foreach ($toolurls as $id => $value) { if ($value == $data['toolurl']) { $ntypeid = $id; break; } } if ($ntypeid === false) { $ntypeid = $DB->get_field('lti_types_config', 'typeid', array(), IGNORE_MULTIPLE); } if ($ntypeid === false) { $ntypeid = 0; } $data['typeid'] = $ntypeid; } if (empty($data['servicesalt'])) { $data['servicesalt'] = uniqid('', true); } foreach ($data as $field => $value) { if (!in_array($field, $ignorefields)) { $this->xmlwriter->full_tag($field, $value); } } return $data; } /** * This is executed when we reach the closing tag of our 'lti' path */ public function on_basiclti_end() { // Finish writing basiclti.xml. $this->xmlwriter->end_tag('lti'); $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); // Write inforef.xml. $this->open_xml_writer("activities/lti_{$this->moduleid}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); foreach ($this->fileman->get_fileids() as $fileid) { $this->write_xml('file', array('id' => $fileid)); } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } } contentitem.php000064400000004671152265520610007622 0ustar00. /** * Handle sending a user to a tool provider to initiate a content-item selection. * * @package mod_lti * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->dirroot . '/mod/lti/lib.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $id = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $title = optional_param('title', '', PARAM_TEXT); $text = optional_param('text', '', PARAM_RAW); $config = lti_get_type_type_config($id); if ($config->lti_ltiversion === LTI_VERSION_1P3) { if (!isset($SESSION->lti_initiatelogin_status)) { echo lti_initiate_login($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text); exit; } else { unset($SESSION->lti_initiatelogin_status); } } // Check access and capabilities. $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); require_login($course); $context = context_course::instance($courseid); require_capability('moodle/course:manageactivities', $context); require_capability('mod/lti:addcoursetool', $context); // Set the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. $returnurlparams = [ 'course' => $course->id, 'id' => $id, 'sesskey' => sesskey() ]; $returnurl = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams); // Prepare the request. $request = lti_build_content_item_selection_request($id, $course, $returnurl, $title, $text, [], []); // Get the launch HTML. $content = lti_post_launch_html($request->params, $request->url, false); echo $content; view.php000064400000021334152265520610006236 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains all necessary code to view a lti activity instance * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or $l = optional_param('l', 0, PARAM_INT); // lti ID. $action = optional_param('action', '', PARAM_TEXT); $foruserid = optional_param('user', 0, PARAM_INT); $forceview = optional_param('forceview', 0, PARAM_BOOL); if ($l) { // Two ways to specify the module. $lti = $DB->get_record('lti', array('id' => $l), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('lti', $lti->id, $lti->course, false, MUST_EXIST); } else { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST); } $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); $typeid = $lti->typeid; if (empty($typeid) && ($tool = lti_get_tool_by_url_match($lti->toolurl))) { $typeid = $tool->id; } if ($typeid) { $toolconfig = lti_get_type_config($typeid); $missingtooltype = empty($toolconfig); if (!$missingtooltype) { $toolurl = $toolconfig['toolurl']; } } else { $toolconfig = array(); $toolurl = $lti->toolurl; } $PAGE->set_cm($cm, $course); // Set's up global $COURSE. $context = context_module::instance($cm->id); $PAGE->set_context($context); require_login($course, true, $cm); require_capability('mod/lti:view', $context); if (!empty($foruserid) && (int)$foruserid !== (int)$USER->id) { require_capability('gradereport/grader:view', $context); } $url = new moodle_url('/mod/lti/view.php', array('id' => $cm->id)); $PAGE->set_url($url); if (!empty($missingtooltype)) { $PAGE->set_pagelayout('incourse'); echo $OUTPUT->header(); throw new moodle_exception('tooltypenotfounderror', 'mod_lti'); } $launchcontainer = lti_get_launch_container($lti, $toolconfig); if ($launchcontainer == LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS) { $PAGE->set_pagelayout('incourse'); $PAGE->blocks->show_only_fake_blocks(); // Disable blocks for layouts which do include pre-post blocks. } else if ($launchcontainer == LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW) { if (!$forceview) { $url = new moodle_url('/mod/lti/launch.php', array('id' => $cm->id)); redirect($url); } } else { // Handles LTI_LAUNCH_CONTAINER_DEFAULT, LTI_LAUNCH_CONTAINER_EMBED, LTI_LAUNCH_CONTAINER_WINDOW. $PAGE->set_pagelayout('incourse'); } lti_view($lti, $course, $cm, $context); $pagetitle = strip_tags($course->shortname.': '.format_string($lti->name)); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); $activityheader = $PAGE->activityheader; if (!$lti->showtitlelaunch) { $header['title'] = ''; } if (!$lti->showdescriptionlaunch) { $header['description'] = ''; } $activityheader->set_attrs($header ?? []); // Print the page header. echo $OUTPUT->header(); if ($typeid) { $config = lti_get_type_type_config($typeid); } else { $config = new stdClass(); $config->lti_ltiversion = LTI_VERSION_1; } $launchurl = new moodle_url('/mod/lti/launch.php', ['id' => $cm->id, 'triggerview' => 0]); if ($action) { $launchurl->param('action', $action);; } if ($foruserid) { $launchurl->param('user', $foruserid);; } unset($SESSION->lti_initiatelogin_status); if (($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW)) { if (!$forceview) { echo "\n"; echo "

".get_string("basiclti_in_new_window", "lti")."

\n"; } echo html_writer::start_tag('p'); echo html_writer::link($launchurl->out(false), get_string("basiclti_in_new_window_open", "lti"), array('target' => '_blank')); echo html_writer::end_tag('p'); } else { $content = ''; // Build the allowed URL, since we know what it will be from $lti->toolurl, // If the specified toolurl is invalid the iframe won't load, but we still want to avoid parse related errors here. // So we set an empty default allowed url, and only build a real one if the parse is successful. $ltiallow = ''; $urlparts = parse_url($toolurl); if ($urlparts && array_key_exists('scheme', $urlparts) && array_key_exists('host', $urlparts)) { $ltiallow = $urlparts['scheme'] . '://' . $urlparts['host']; // If a port has been specified we append that too. if (array_key_exists('port', $urlparts)) { $ltiallow .= ':' . $urlparts['port']; } } // Request the launch content with an iframe tag. $attributes = []; $attributes['id'] = "contentframe"; $attributes['height'] = '600px'; $attributes['width'] = '100%'; $attributes['src'] = $launchurl; $attributes['allow'] = "microphone $ltiallow; " . "camera $ltiallow; " . "geolocation $ltiallow; " . "midi $ltiallow; " . "encrypted-media $ltiallow; " . "autoplay $ltiallow;"; // Only allow local-network-access if it is enabled in config.php. if (isset($CFG->ltiallowlocalnetwork) && $CFG->ltiallowlocalnetwork) { $attributes['allow'] .= " local-network-access $ltiallow;"; } $attributes['allowfullscreen'] = 1; $iframehtml = html_writer::tag('iframe', $content, $attributes); echo $iframehtml; // Output script to make the iframe tag be as large as possible. $resize = ' '; echo $resize; } // Finish the page. echo $OUTPUT->footer(); edit_form.php000064400000053477152265520610007251 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines de main basiclti configuration form * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Charles Severance * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * LTI Edit Form * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_lti_edit_types_form extends moodleform { /** * Define this form. */ public function definition() { global $CFG, $PAGE, $DB, $OUTPUT; $mform =& $this->_form; $istool = $this->_customdata && isset($this->_customdata->istool) && $this->_customdata->istool; $typeid = $this->_customdata->id ?? ''; $clientid = $this->_customdata->clientid ?? ''; // Add basiclti elements. $mform->addElement('header', 'setup', get_string('tool_settings', 'lti')); $mform->addElement('text', 'lti_typename', get_string('typename', 'lti')); $mform->setType('lti_typename', PARAM_TEXT); $mform->addHelpButton('lti_typename', 'typename', 'lti'); $mform->addRule('lti_typename', null, 'required', null, 'client'); $mform->addElement('text', 'lti_toolurl', get_string('toolurl', 'lti'), array('size' => '64')); $mform->setType('lti_toolurl', PARAM_URL); $mform->addHelpButton('lti_toolurl', 'toolurl', 'lti'); $mform->addElement('textarea', 'lti_description', get_string('tooldescription', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('lti_description', PARAM_TEXT); $mform->addHelpButton('lti_description', 'tooldescription', 'lti'); if (!$istool) { $mform->addRule('lti_toolurl', null, 'required', null, 'client'); } else { $mform->disabledIf('lti_toolurl', null); } if (!$istool) { $options = array( LTI_VERSION_1 => get_string('oauthsecurity', 'lti'), LTI_VERSION_1P3 => get_string('jwtsecurity', 'lti'), ); $mform->addElement('select', 'lti_ltiversion', get_string('ltiversion', 'lti'), $options); $mform->setType('lti_ltiversion', PARAM_TEXT); $mform->addHelpButton('lti_ltiversion', 'ltiversion', 'lti'); $mform->setDefault('lti_ltiversion', LTI_VERSION_1); $mform->addElement('text', 'lti_resourcekey', get_string('resourcekey_admin', 'lti')); $mform->setType('lti_resourcekey', PARAM_TEXT); $mform->addHelpButton('lti_resourcekey', 'resourcekey_admin', 'lti'); $mform->hideIf('lti_resourcekey', 'lti_ltiversion', 'eq', LTI_VERSION_1P3); $mform->setForceLtr('lti_resourcekey'); $mform->addElement('passwordunmask', 'lti_password', get_string('password_admin', 'lti')); $mform->setType('lti_password', PARAM_RAW); $mform->addHelpButton('lti_password', 'password_admin', 'lti'); $mform->hideIf('lti_password', 'lti_ltiversion', 'eq', LTI_VERSION_1P3); if (!empty($typeid)) { $mform->addElement('text', 'lti_clientid_disabled', get_string('clientidadmin', 'lti')); $mform->setType('lti_clientid_disabled', PARAM_TEXT); $mform->addHelpButton('lti_clientid_disabled', 'clientidadmin', 'lti'); $mform->hideIf('lti_clientid_disabled', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->disabledIf('lti_clientid_disabled', null); $mform->setForceLtr('lti_clientid_disabled'); $mform->addElement('hidden', 'lti_clientid'); $mform->setType('lti_clientid', PARAM_TEXT); } $keyoptions = [ LTI_RSA_KEY => get_string('keytype_rsa', 'lti'), LTI_JWK_KEYSET => get_string('keytype_keyset', 'lti'), ]; $mform->addElement('select', 'lti_keytype', get_string('keytype', 'lti'), $keyoptions); $mform->setType('lti_keytype', PARAM_TEXT); $mform->addHelpButton('lti_keytype', 'keytype', 'lti'); $mform->setDefault('lti_keytype', LTI_JWK_KEYSET); $mform->hideIf('lti_keytype', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->addElement('textarea', 'lti_publickey', get_string('publickey', 'lti'), ['rows' => 8, 'cols' => 60]); $mform->setType('lti_publickey', PARAM_TEXT); $mform->addHelpButton('lti_publickey', 'publickey', 'lti'); $mform->hideIf('lti_publickey', 'lti_keytype', 'neq', LTI_RSA_KEY); $mform->hideIf('lti_publickey', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->setForceLtr('lti_publickey'); $mform->addElement('text', 'lti_publickeyset', get_string('publickeyset', 'lti'), ['size' => '64']); $mform->setType('lti_publickeyset', PARAM_TEXT); $mform->addHelpButton('lti_publickeyset', 'publickeyset', 'lti'); $mform->hideIf('lti_publickeyset', 'lti_keytype', 'neq', LTI_JWK_KEYSET); $mform->hideIf('lti_publickeyset', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->setForceLtr('lti_publickeyset'); $mform->addElement('text', 'lti_initiatelogin', get_string('initiatelogin', 'lti'), array('size' => '64')); $mform->setType('lti_initiatelogin', PARAM_URL); $mform->addHelpButton('lti_initiatelogin', 'initiatelogin', 'lti'); $mform->hideIf('lti_initiatelogin', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->addElement('textarea', 'lti_redirectionuris', get_string('redirectionuris', 'lti'), array('rows' => 3, 'cols' => 60)); $mform->setType('lti_redirectionuris', PARAM_TEXT); $mform->addHelpButton('lti_redirectionuris', 'redirectionuris', 'lti'); $mform->hideIf('lti_redirectionuris', 'lti_ltiversion', 'neq', LTI_VERSION_1P3); $mform->setForceLtr('lti_redirectionuris'); } if ($istool) { $mform->addElement('textarea', 'lti_parameters', get_string('parameter', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('lti_parameters', PARAM_TEXT); $mform->addHelpButton('lti_parameters', 'parameter', 'lti'); $mform->disabledIf('lti_parameters', null); $mform->setForceLtr('lti_parameters'); } $mform->addElement('textarea', 'lti_customparameters', get_string('custom', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('lti_customparameters', PARAM_TEXT); $mform->addHelpButton('lti_customparameters', 'custom', 'lti'); $mform->setForceLtr('lti_customparameters'); if (!empty($this->_customdata->isadmin)) { // Only site-level preconfigured tools allow the control of course visibility in the site admin tool type form. if (empty($this->_customdata->iscoursetool) || !$this->_customdata->iscoursetool) { $options = array( LTI_COURSEVISIBLE_NO => get_string('show_in_course_no', 'lti'), LTI_COURSEVISIBLE_PRECONFIGURED => get_string('show_in_course_preconfigured', 'lti'), LTI_COURSEVISIBLE_ACTIVITYCHOOSER => get_string('show_in_course_activity_chooser', 'lti'), ); if ($istool) { // LTI2 tools can not be matched by URL, they have to be either in preconfigured tools or in activity chooser. unset($options[LTI_COURSEVISIBLE_NO]); $stringname = 'show_in_course_lti2'; } else { $stringname = 'show_in_course_lti1'; } $mform->addElement('select', 'lti_coursevisible', get_string($stringname, 'lti'), $options); $mform->addHelpButton('lti_coursevisible', $stringname, 'lti'); $mform->setDefault('lti_coursevisible', '1'); } } else { $mform->addElement('hidden', 'lti_coursevisible', LTI_COURSEVISIBLE_ACTIVITYCHOOSER); } $mform->setType('lti_coursevisible', PARAM_INT); $mform->addElement('hidden', 'typeid'); $mform->setType('typeid', PARAM_INT); $launchoptions = array(); $launchoptions[LTI_LAUNCH_CONTAINER_EMBED] = get_string('embed', 'lti'); $launchoptions[LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS] = get_string('embed_no_blocks', 'lti'); $launchoptions[LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW] = get_string('existing_window', 'lti'); $launchoptions[LTI_LAUNCH_CONTAINER_WINDOW] = get_string('new_window', 'lti'); $mform->addElement('select', 'lti_launchcontainer', get_string('default_launch_container', 'lti'), $launchoptions); $mform->setDefault('lti_launchcontainer', LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS); $mform->addHelpButton('lti_launchcontainer', 'default_launch_container', 'lti'); $mform->setType('lti_launchcontainer', PARAM_INT); $mform->addElement('advcheckbox', 'lti_contentitem', get_string('contentitem_deeplinking', 'lti')); $mform->addHelpButton('lti_contentitem', 'contentitem_deeplinking', 'lti'); if ($istool) { $mform->disabledIf('lti_contentitem', null); } $mform->addElement('text', 'lti_toolurl_ContentItemSelectionRequest', get_string('toolurl_contentitemselectionrequest', 'lti'), array('size' => '64')); $mform->setType('lti_toolurl_ContentItemSelectionRequest', PARAM_URL); $mform->addHelpButton('lti_toolurl_ContentItemSelectionRequest', 'toolurl_contentitemselectionrequest', 'lti'); $mform->disabledIf('lti_toolurl_ContentItemSelectionRequest', 'lti_contentitem', 'notchecked'); if ($istool) { $mform->disabledIf('lti_toolurl__ContentItemSelectionRequest', null); } $mform->addElement('hidden', 'oldicon'); $mform->setType('oldicon', PARAM_URL); $mform->addElement('text', 'lti_icon', get_string('icon_url', 'lti'), array('size' => '64')); $mform->setType('lti_icon', PARAM_URL); $mform->setAdvanced('lti_icon'); $mform->addHelpButton('lti_icon', 'icon_url', 'lti'); $mform->addElement('text', 'lti_secureicon', get_string('secure_icon_url', 'lti'), array('size' => '64')); $mform->setType('lti_secureicon', PARAM_URL); $mform->setAdvanced('lti_secureicon'); $mform->addHelpButton('lti_secureicon', 'secure_icon_url', 'lti'); // Restrict to course categories. if (empty($this->_customdata->iscoursetool) || !$this->_customdata->iscoursetool) { $mform->addElement('header', 'coursecategory', get_string('restricttocategory', 'lti')); $mform->addHelpButton('coursecategory', 'restricttocategory', 'lti'); $records = $DB->get_records('course_categories', [], 'sortorder, id', 'id,parent,name'); // Convert array of objects to two dimentional array. $tree = $this->lti_build_category_tree(array_map(fn($record) => (array)$record, $records)); $mform->addElement('html', $OUTPUT->render_from_template('mod_lti/categorynode', ['nodes' => $tree])); $mform->addElement('hidden', 'lti_coursecategories'); $mform->setType('lti_coursecategories', PARAM_TEXT); } if (!$istool) { // Display the lti advantage services. $this->get_lti_advantage_services($mform); } if (!$istool) { // Add privacy preferences fieldset where users choose whether to send their data. $mform->addElement('header', 'privacy', get_string('privacy', 'lti')); $options = array(); $options[0] = get_string('never', 'lti'); $options[1] = get_string('always', 'lti'); $mform->addElement('select', 'lti_sendname', get_string('share_name_admin', 'lti'), $options); $mform->setType('lti_sendname', PARAM_INT); $mform->setDefault('lti_sendname', '0'); $mform->addHelpButton('lti_sendname', 'share_name_admin', 'lti'); $mform->addElement('select', 'lti_sendemailaddr', get_string('share_email_admin', 'lti'), $options); $mform->setType('lti_sendemailaddr', PARAM_INT); $mform->setDefault('lti_sendemailaddr', '0'); $mform->addHelpButton('lti_sendemailaddr', 'share_email_admin', 'lti'); // LTI Extensions. // Add grading preferences fieldset where the tool is allowed to return grades. $gradeoptions = array(); $gradeoptions[] = get_string('never', 'lti'); $gradeoptions[] = get_string('always', 'lti'); $gradeoptions[] = get_string('delegate_tool', 'lti'); $mform->addElement('select', 'lti_acceptgrades', get_string('accept_grades_admin', 'lti'), $gradeoptions); $mform->setType('lti_acceptgrades', PARAM_INT); $mform->setDefault('lti_acceptgrades', '2'); $mform->addHelpButton('lti_acceptgrades', 'accept_grades_admin', 'lti'); $mform->addElement('checkbox', 'lti_forcessl', get_string('force_ssl', 'lti')); $mform->setType('lti_forcessl', PARAM_BOOL); if (!empty($CFG->mod_lti_forcessl)) { $mform->setDefault('lti_forcessl', '1'); $mform->freeze('lti_forcessl'); } else { $mform->setDefault('lti_forcessl', '0'); } $mform->addHelpButton('lti_forcessl', 'force_ssl', 'lti'); if (!empty($this->_customdata->isadmin)) { // Add setup parameters fieldset. $mform->addElement('header', 'setupoptions', get_string('miscellaneous', 'lti')); $options = array( LTI_DEFAULT_ORGID_SITEID => get_string('siteid', 'lti'), LTI_DEFAULT_ORGID_SITEHOST => get_string('sitehost', 'lti'), ); $mform->addElement('select', 'lti_organizationid_default', get_string('organizationid_default', 'lti'), $options); $mform->setType('lti_organizationid_default', PARAM_TEXT); $mform->setDefault('lti_organizationid_default', LTI_DEFAULT_ORGID_SITEID); $mform->addHelpButton('lti_organizationid_default', 'organizationid_default', 'lti'); $mform->addElement('text', 'lti_organizationid', get_string('organizationidguid', 'lti')); $mform->setType('lti_organizationid', PARAM_TEXT); $mform->addHelpButton('lti_organizationid', 'organizationidguid', 'lti'); $mform->addElement('text', 'lti_organizationurl', get_string('organizationurl', 'lti')); $mform->setType('lti_organizationurl', PARAM_URL); $mform->addHelpButton('lti_organizationurl', 'organizationurl', 'lti'); } } /* Suppress this for now - Chuck * mform->addElement('text', 'lti_organizationdescr', get_string('organizationdescr', 'lti')) * mform->setType('lti_organizationdescr', PARAM_TEXT) * mform->addHelpButton('lti_organizationdescr', 'organizationdescr', 'lti') */ /* // Add a hidden element to signal a tool fixing operation after a problematic backup - restore process //$mform->addElement('hidden', 'lti_fix'); */ $tab = optional_param('tab', '', PARAM_ALPHAEXT); $mform->addElement('hidden', 'tab', $tab); $mform->setType('tab', PARAM_ALPHAEXT); $courseid = optional_param('course', 1, PARAM_INT); $mform->addElement('hidden', 'course', $courseid); $mform->setType('course', PARAM_INT); // Add standard buttons, common to all modules. $this->add_action_buttons(); } /** * Retrieves the data of the submitted form. * * @return stdClass */ public function get_data() { $data = parent::get_data(); if ($data && !empty($this->_customdata->istool)) { // Content item checkbox is disabled in tool settings, so this cannot be edited. Just unset it. unset($data->lti_contentitem); } return $data; } /** * Generates the lti advantage extra configuration adding it to the mform * * @param MoodleQuickForm $mform */ public function get_lti_advantage_services(&$mform) { // For each service add the label and get the array of configuration. $services = lti_get_services(); $mform->addElement('header', 'services', get_string('services', 'lti')); foreach ($services as $service) { /** @var \mod_lti\local\ltiservice\service_base $service */ $service->get_configuration_options($mform); } } /** * Validate the form data before we allow them to save the tool type. * * @param array $data * @param array $files * @return array Error messages */ public function validation($data, $files) { global $CFG; $errors = parent::validation($data, $files); // LTI2 tools do not contain a ltiversion field. if (isset($data['lti_ltiversion']) && $data['lti_ltiversion'] == LTI_VERSION_1P3) { require_once($CFG->dirroot . '/mod/lti/upgradelib.php'); $warning = mod_lti_verify_private_key(); if (!empty($warning)) { $errors['lti_ltiversion'] = $warning; return $errors; } } return $errors; } /** * Build category tree. * * @param array $elements * @param int $parentid * @return array category tree */ public function lti_build_category_tree(array $elements, int $parentid = 0): array { $branch = []; foreach ($elements as $element) { if ($element['parent'] == $parentid) { $children = $this->lti_build_category_tree($elements, $element['id']); if ($children) { $element['nodes'] = $children; $element['haschildren'] = true; } else { $element['nodes'] = null; $element['haschildren'] = false; } $branch[] = $element; } } return $branch; } public function definition_after_data() { // Add the deprecated "Delegate to teacher" option to the "Share launcher's name" and "Share launcher's email" fields for // existing types which are already using this setting value. This ensures that editing the tool type won't result in a // change to the existing value. Add the option as a disabled to make this clear. Once changed, it cannot be set again. // This isn't supported from 4.3 onward in the creation of new tool types. foreach (['lti_sendname', 'lti_sendemailaddr'] as $elementname) { if (!empty($this->_form->_defaultValues[$elementname]) && $this->_form->_defaultValues[$elementname] == LTI_SETTING_DELEGATE) { $elementarr = array_filter($this->_form->_elements, function ($element) use($elementname) { return !empty($element->_attributes['name']) && $element->_attributes['name'] == $elementname; }); /** @var MoodleQuickForm_select $element */ $element = array_shift($elementarr); $element->addOption( get_string('delegate', 'mod_lti'), LTI_SETTING_DELEGATE, ['disabled' => 'disabled', 'selected' => 'selected'] ); } } } } source/upgrade.txt000064400000001463152265520610010244 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /mod/lti/source/* - LTI source plugins, information provided here is intended especially for developers. === 4.1 === * The callback get_shortcuts() is now removed. Please use get_course_content_items and get_all_content_items instead. === 3.9 === * The callback get_shortcuts() is now deprecated. Please use get_course_content_items and get_all_content_items instead. See source code examples in get_course_content_items() and get_all_content_items() in mod/lti/lib.php for details. === 3.1 === * Callback get_types() is deprecated, instead ltisource plugins can define callback get_shortcuts(). See source code for lti_get_shortcuts() and get_module_metadata(). source/readme.txt000064400000002024152265520610010044 0ustar00// 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 . /** * Readme file for local customisations * * @package mod_lti * @copyright 2012 Mark Nielsen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ LTI Source plugins directory ============================== This directory contains LTI source plugins which are plugins that can register custom handlers to process lti messages. pix/warning.svg000064400000000626152265520610007542 0ustar00 pix/monologo.svg000064400000006427152265520610007733 0ustar00 registrationreturn.php000064400000006743152265520610011245 0ustar00. /** * Handle the return from the Tool Provider after registering a tool proxy. * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $top = optional_param('top', 0, PARAM_INT); $msg = optional_param('lti_msg', '', PARAM_TEXT); $err = optional_param('lti_errormsg', '', PARAM_TEXT); $id = optional_param('id', 0, PARAM_INT); // No guest autologin. require_sesskey(); require_login(0, false); $systemcontext = context_system::instance(); require_capability('moodle/site:config', $systemcontext); if (empty($top)) { $params = array(); $params['sesskey'] = sesskey(); $params['top'] = '1'; if (!empty($msg)) { $params['lti_msg'] = $msg; } if (!empty($err)) { $params['lti_errormsg'] = $err; } if (!empty($id)) { $params['id'] = $id; } $redirect = new moodle_url('/mod/lti/registrationreturn.php', $params); $redirect = $redirect->out(false); $clickhere = get_string('click_to_continue', 'lti', (object)array('link' => $redirect)); $html = <<< EOD EOD; // We always send the headers because they set the encoding. send_headers('text/html; charset=utf-8', false); echo $html; } else if (!empty($msg) && !empty($err)) { $params = array(); $params['sesskey'] = sesskey(); $params['top'] = '1'; if (!empty($err)) { $params['lti_errormsg'] = $err; } if (!empty($id)) { $params['id'] = $id; } $redirect = new moodle_url('/mod/lti/registrationreturn.php', $params); $redirect = $redirect->out(false); redirect($redirect, $err); } else { $redirect = new moodle_url('/mod/lti/toolproxies.php'); if (!empty($id)) { $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id)); switch($toolproxy->state) { case LTI_TOOL_PROXY_STATE_ACCEPTED: $redirect->param('tab', 'tp_accepted'); break; case LTI_TOOL_PROXY_STATE_REJECTED: $redirect->param('tab', 'tp_rejected'); break; case LTI_TOOL_PROXY_STATE_PENDING: // Change the status to configured. $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; lti_update_tool_proxy($toolproxy); } } $redirect = $redirect->out(); if (empty($msg)) { $msg = $err; } redirect($redirect, $msg); } openid-configuration.php000064400000005247152265520610011414 0ustar00. /** * This file returns the OpenId/LTI Configuration for this site. * * It is part of the LTI Tool Dynamic Registration, and used by * tools to get the site configuration and registration end-point. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_lti\local\ltiopenid\registration_helper; define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->libdir.'/weblib.php'); $scopes = registration_helper::get()->lti_get_service_scopes(); $scopes[] = 'openid'; $conf = [ 'issuer' => $CFG->wwwroot, 'token_endpoint' => (new moodle_url('/mod/lti/token.php'))->out(false), 'token_endpoint_auth_methods_supported' => ['private_key_jwt'], 'token_endpoint_auth_signing_alg_values_supported' => ['RS256'], 'jwks_uri' => (new moodle_url('/mod/lti/certs.php'))->out(false), 'authorization_endpoint' => (new moodle_url('/mod/lti/auth.php'))->out(false), 'registration_endpoint' => (new moodle_url('/mod/lti/openid-registration.php'))->out(false), 'scopes_supported' => $scopes, 'response_types_supported' => ['id_token'], 'subject_types_supported' => ['public', 'pairwise'], 'id_token_signing_alg_values_supported' => ['RS256'], 'claims_supported' => ['sub', 'iss', 'name', 'given_name', 'family_name', 'email'], 'https://purl.imsglobal.org/spec/lti-platform-configuration' => [ 'product_family_code' => 'moodle', 'version' => $CFG->release, 'messages_supported' => [['type' => 'LtiResourceLinkRequest'], ['type' => 'LtiDeepLinkingRequest', 'placements' => ['ContentArea']]], 'variables' => array_keys(lti_get_capabilities()) ] ]; @header('Content-Type: application/json; charset=utf-8'); echo json_encode($conf, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); register_form.php000064400000010522152265520610010130 0ustar00. /** * This file defines the main tool registration configuration form * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * The mod_lti_register_types_form class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_lti_register_types_form extends moodleform { /** * Set up the form definition. */ public function definition() { global $CFG; $mform =& $this->_form; $mform->addElement('header', 'setup', get_string('registration_options', 'lti')); // Tool Provider name. $strrequired = get_string('required'); $mform->addElement('text', 'lti_registrationname', get_string('registrationname', 'lti')); $mform->setType('lti_registrationname', PARAM_TEXT); $mform->addHelpButton('lti_registrationname', 'registrationname', 'lti'); $mform->addRule('lti_registrationname', $strrequired, 'required', null, 'client'); // Registration URL. $mform->addElement('text', 'lti_registrationurl', get_string('registrationurl', 'lti'), array('size' => '64')); $mform->setType('lti_registrationurl', PARAM_URL); $mform->addHelpButton('lti_registrationurl', 'registrationurl', 'lti'); $mform->addRule('lti_registrationurl', $strrequired, 'required', null, 'client'); // LTI Capabilities. $options = array_keys(lti_get_capabilities()); natcasesort($options); $attributes = array( 'multiple' => 1, 'size' => min(count($options), 10) ); $mform->addElement('select', 'lti_capabilities', get_string('capabilities', 'lti'), array_combine($options, $options), $attributes); $mform->setType('lti_capabilities', PARAM_TEXT); $mform->addHelpButton('lti_capabilities', 'capabilities', 'lti'); $mform->addRule('lti_capabilities', $strrequired, 'required', null, 'client'); // LTI Services. $services = lti_get_services(); $options = array(); foreach ($services as $service) { $options[$service->get_id()] = $service->get_name(); } $attributes = array( 'multiple' => 1, 'size' => min(count($options), 10) ); $mform->addElement('select', 'lti_services', get_string('services', 'lti'), $options, $attributes); $mform->setType('lti_services', PARAM_TEXT); $mform->addHelpButton('lti_services', 'services', 'lti'); $mform->addRule('lti_services', $strrequired, 'required', null, 'client'); $mform->addElement('hidden', 'toolproxyid'); $mform->setType('toolproxyid', PARAM_INT); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $mform->addElement('hidden', 'tab', $tab); $mform->setType('tab', PARAM_ALPHAEXT); $courseid = optional_param('course', 1, PARAM_INT); $mform->addElement('hidden', 'course', $courseid); $mform->setType('course', PARAM_INT); // Add standard buttons, common to all modules. $this->add_action_buttons(); } /** * Set up rules for disabling fields. */ public function disable_fields() { $mform =& $this->_form; $mform->disabledIf('lti_registrationurl', null); $mform->disabledIf('lti_capabilities', null); $mform->disabledIf('lti_services', null); } } service.php000064400000016530152265520610006726 0ustar00. /** * LTI web service endpoints * * @package mod_lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Chris Scribner */ define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . "/../../config.php"); require_once($CFG->dirroot.'/mod/lti/locallib.php'); require_once($CFG->dirroot.'/mod/lti/servicelib.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use mod_lti\service_exception_handler; use moodle\mod\lti as lti; use ltiservice_basicoutcomes\local\service\basicoutcomes; $rawbody = file_get_contents("php://input"); $logrequests = lti_should_log_request($rawbody); $errorhandler = new service_exception_handler($logrequests); // Register our own error handler so we can always send valid XML response. set_exception_handler(array($errorhandler, 'handle')); if ($logrequests) { lti_log_request($rawbody); } $ok = true; $type = null; $toolproxy = false; $consumerkey = lti\get_oauth_key_from_headers(null, array(basicoutcomes::SCOPE_BASIC_OUTCOMES)); if ($consumerkey === false) { throw new Exception('Missing or invalid consumer key or access token.'); } else if (is_string($consumerkey)) { $toolproxy = lti_get_tool_proxy_from_guid($consumerkey); if ($toolproxy !== false) { $secrets = array($toolproxy->secret); } else if (!empty($tool)) { $secrets = array($typeconfig['password']); } else { $secrets = lti_get_shared_secrets_by_key($consumerkey); } $sharedsecret = lti_verify_message($consumerkey, lti_get_shared_secrets_by_key($consumerkey), $rawbody); if ($sharedsecret === false) { throw new Exception('Message signature not valid'); } } // TODO MDL-46023 Replace this code with a call to the new library. $xml = simplexml_load_string($rawbody); if (!$xml) { throw new Exception('Invalid XML content'); } $body = $xml->imsx_POXBody; foreach ($body->children() as $child) { $messagetype = $child->getName(); } // We know more about the message, update error handler to send better errors. $errorhandler->set_message_id(lti_parse_message_id($xml)); $errorhandler->set_message_type($messagetype); switch ($messagetype) { case 'replaceResultRequest': $parsed = lti_parse_grade_replace_message($xml); $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid)); if (!lti_accepts_grades($ltiinstance)) { throw new Exception('Tool does not accept grades'); } lti_verify_sourcedid($ltiinstance, $parsed); lti_set_session_user($parsed->userid); $gradestatus = lti_update_grade($ltiinstance, $parsed->userid, $parsed->launchid, $parsed->gradeval); if (!$gradestatus) { throw new Exception('Grade replace response'); } $responsexml = lti_get_response_xml( 'success', 'Grade replace response', $parsed->messageid, 'replaceResultResponse' ); echo $responsexml->asXML(); break; case 'readResultRequest': $parsed = lti_parse_grade_read_message($xml); $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid)); if (!lti_accepts_grades($ltiinstance)) { throw new Exception('Tool does not accept grades'); } // Getting the grade requires the context is set. $context = context_course::instance($ltiinstance->course); $PAGE->set_context($context); lti_verify_sourcedid($ltiinstance, $parsed); $grade = lti_read_grade($ltiinstance, $parsed->userid); $responsexml = lti_get_response_xml( 'success', // Empty grade is also 'success'. 'Result read', $parsed->messageid, 'readResultResponse' ); $node = $responsexml->imsx_POXBody->readResultResponse; $node = $node->addChild('result')->addChild('resultScore'); $node->addChild('language', 'en'); $node->addChild('textString', isset($grade) ? $grade : ''); echo $responsexml->asXML(); break; case 'deleteResultRequest': $parsed = lti_parse_grade_delete_message($xml); $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid)); if (!lti_accepts_grades($ltiinstance)) { throw new Exception('Tool does not accept grades'); } lti_verify_sourcedid($ltiinstance, $parsed); lti_set_session_user($parsed->userid); $gradestatus = lti_delete_grade($ltiinstance, $parsed->userid); if (!$gradestatus) { throw new Exception('Grade delete request'); } $responsexml = lti_get_response_xml( 'success', 'Grade delete request', $parsed->messageid, 'deleteResultResponse' ); echo $responsexml->asXML(); break; default: // Fire an event if we get a web service request which we don't support directly. // This will allow others to extend the LTI services, which I expect to be a common // use case, at least until the spec matures. $data = new stdClass(); $data->body = $rawbody; $data->xml = $xml; $data->messageid = lti_parse_message_id($xml); $data->messagetype = $messagetype; $data->consumerkey = $consumerkey; $data->sharedsecret = $sharedsecret; $eventdata = array(); $eventdata['other'] = array(); $eventdata['other']['messageid'] = $data->messageid; $eventdata['other']['messagetype'] = $messagetype; $eventdata['other']['consumerkey'] = $consumerkey; // Before firing the event, allow subplugins a chance to handle. if (lti_extend_lti_services($data)) { break; } // If an event handler handles the web service, it should set this global to true // So this code knows whether to send an "operation not supported" or not. global $ltiwebservicehandled; $ltiwebservicehandled = false; try { $event = \mod_lti\event\unknown_service_api_called::create($eventdata); $event->set_message_data($data); $event->trigger(); } catch (Exception $e) { $ltiwebservicehandled = false; } if (!$ltiwebservicehandled) { $responsexml = lti_get_response_xml( 'unsupported', 'unsupported', lti_parse_message_id($xml), $messagetype ); echo $responsexml->asXML(); } break; } OAuth.php000064400000072361152265520610006312 0ustar00. /** * This file contains the OAuth 1.0a implementation used for support for LTI 1.1. * * @package mod_lti * @copyright moodle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace moodle\mod\lti;//Using a namespace as the basicLTI module imports classes with the same names defined('MOODLE_INTERNAL') || die; $lastcomputedsignature = false; /** * Generic exception class */ class OAuthException extends \Exception { // pass } /** * OAuth 1.0 Consumer class */ class OAuthConsumer { public $key; public $secret; /** @var string|null callback URL. */ public ?string $callback_url; function __construct($key, $secret, $callback_url = null) { $this->key = $key; $this->secret = $secret; $this->callback_url = $callback_url; } function __toString() { return "OAuthConsumer[key=$this->key,secret=$this->secret]"; } } class OAuthToken { // access tokens and request tokens public $key; public $secret; /** * key = the token * secret = the token secret */ function __construct($key, $secret) { $this->key = $key; $this->secret = $secret; } /** * generates the basic string serialization of a token that a server * would respond to request_token and access_token calls with */ function to_string() { return "oauth_token=" . OAuthUtil::urlencode_rfc3986($this->key) . "&oauth_token_secret=" . OAuthUtil::urlencode_rfc3986($this->secret); } function __toString() { return $this->to_string(); } } class OAuthSignatureMethod { public function check_signature(&$request, $consumer, $token, $signature) { $built = $this->build_signature($request, $consumer, $token); return $built == $signature; } } /** * Base class for the HMac based signature methods. */ abstract class OAuthSignatureMethod_HMAC extends OAuthSignatureMethod { /** * Name of the Algorithm used. * * @return string algorithm name. */ abstract public function get_name(): string; public function build_signature($request, $consumer, $token) { global $lastcomputedsignature; $lastcomputedsignature = false; $basestring = $request->get_signature_base_string(); $request->base_string = $basestring; $key_parts = array( $consumer->secret, ($token) ? $token->secret : "" ); $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); $key = implode('&', $key_parts); $computedsignature = base64_encode(hash_hmac(strtolower(substr($this->get_name(), 5)), $basestring, $key, true)); $lastcomputedsignature = $computedsignature; return $computedsignature; } } /** * Implementation for SHA 1. */ class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod_HMAC { /** * Name of the Algorithm used. * * @return string algorithm name. */ public function get_name(): string { return "HMAC-SHA1"; } } /** * Implementation for SHA 256. */ class OAuthSignatureMethod_HMAC_SHA256 extends OAuthSignatureMethod_HMAC { /** * Name of the Algorithm used. * * @return string algorithm name. */ public function get_name(): string { return "HMAC-SHA256"; } } class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { /** * Name of the Algorithm used. * * @return string algorithm name. */ public function get_name(): string { return "PLAINTEXT"; } public function build_signature($request, $consumer, $token) { $sig = array( OAuthUtil::urlencode_rfc3986($consumer->secret) ); if ($token) { array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret)); } else { array_push($sig, ''); } $raw = implode("&", $sig); // for debug purposes $request->base_string = $raw; return OAuthUtil::urlencode_rfc3986($raw); } } class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { /** * Name of the Algorithm used. * * @return string algorithm name. */ public function get_name(): string { return "RSA-SHA1"; } protected function fetch_public_cert(&$request) { // not implemented yet, ideas are: // (1) do a lookup in a table of trusted certs keyed off of consumer // (2) fetch via http using a url provided by the requester // (3) some sort of specific discovery code based on request // // either way should return a string representation of the certificate throw new OAuthException("fetch_public_cert not implemented"); } protected function fetch_private_cert(&$request) { // not implemented yet, ideas are: // (1) do a lookup in a table of trusted certs keyed off of consumer // // either way should return a string representation of the certificate throw new OAuthException("fetch_private_cert not implemented"); } public function build_signature(&$request, $consumer, $token) { $base_string = $request->get_signature_base_string(); $request->base_string = $base_string; // Fetch the private key cert based on the request $cert = $this->fetch_private_cert($request); // Pull the private key ID from the certificate $privatekeyid = openssl_get_privatekey($cert); // Sign using the key $ok = openssl_sign($base_string, $signature, $privatekeyid); // Avoid passing null values to base64_encode. if (!$ok) { throw new OAuthException("OpenSSL unable to sign data"); } return base64_encode($signature); } public function check_signature(&$request, $consumer, $token, $signature) { $decoded_sig = base64_decode($signature); $base_string = $request->get_signature_base_string(); // Fetch the public key cert based on the request $cert = $this->fetch_public_cert($request); // Pull the public key ID from the certificate $publickeyid = openssl_get_publickey($cert); // Check the computed signature against the one passed in the query $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); return $ok == 1; } } class OAuthRequest { private $parameters; private $http_method; private $http_url; // for debug purposes public $base_string; public static $version = '1.0'; public static $POST_INPUT = 'php://input'; function __construct($http_method, $http_url, $parameters = null) { @$parameters or $parameters = array(); $this->parameters = $parameters; $this->http_method = $http_method; $this->http_url = $http_url; } /** * attempt to build up a request from what was passed to the server */ public static function from_request($http_method = null, $http_url = null, $parameters = null) { $scheme = (!is_https()) ? 'http' : 'https'; $port = ""; if ($_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" && strpos(':', $_SERVER['HTTP_HOST']) < 0) { $port = ':' . $_SERVER['SERVER_PORT']; } @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $port . $_SERVER['REQUEST_URI']; @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; // We weren't handed any parameters, so let's find the ones relevant to // this request. // If you run XML-RPC or similar you should use this to provide your own // parsed parameter-list if (!$parameters) { // Find request headers $request_headers = OAuthUtil::get_headers(); // Parse the query-string to find GET parameters $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); $ourpost = $_POST; // Add POST Parameters if they exist $parameters = array_merge($parameters, $ourpost); // We have a Authorization-header with OAuth data. Parse the header // and add those overriding any duplicates from GET or POST if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { $header_parameters = OAuthUtil::split_header($request_headers['Authorization']); $parameters = array_merge($parameters, $header_parameters); } } return new OAuthRequest($http_method, $http_url, $parameters); } /** * pretty much a helper function to set up the request */ public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters = null) { @$parameters or $parameters = array(); $defaults = array( "oauth_version" => self::$version, "oauth_nonce" => self::generate_nonce(), "oauth_timestamp" => self::generate_timestamp(), "oauth_consumer_key" => $consumer->key ); if ($token) { $defaults['oauth_token'] = $token->key; } $parameters = array_merge($defaults, $parameters); // Parse the query-string to find and add GET parameters $parts = parse_url($http_url); if (isset($parts['query'])) { $qparms = OAuthUtil::parse_parameters($parts['query']); $parameters = array_merge($qparms, $parameters); } return new OAuthRequest($http_method, $http_url, $parameters); } public function set_parameter($name, $value, $allow_duplicates = true) { if ($allow_duplicates && isset($this->parameters[$name])) { // We have already added parameter(s) with this name, so add to the list if (is_scalar($this->parameters[$name])) { // This is the first duplicate, so transform scalar (string) // into an array so we can add the duplicates $this->parameters[$name] = array($this->parameters[$name]); } $this->parameters[$name][] = $value; } else { $this->parameters[$name] = $value; } } public function get_parameter($name) { return isset($this->parameters[$name]) ? $this->parameters[$name] : null; } public function get_parameters() { return $this->parameters; } public function unset_parameter($name) { unset($this->parameters[$name]); } /** * The request parameters, sorted and concatenated into a normalized string. * @return string */ public function get_signable_parameters() { // Grab all parameters $params = $this->parameters; // Remove oauth_signature if present // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") if (isset($params['oauth_signature'])) { unset($params['oauth_signature']); } return OAuthUtil::build_http_query($params); } /** * Returns the base string of this request * * The base string defined as the method, the url * and the parameters (normalized), each urlencoded * and the concated with &. */ public function get_signature_base_string() { $parts = array( $this->get_normalized_http_method(), $this->get_normalized_http_url(), $this->get_signable_parameters() ); $parts = OAuthUtil::urlencode_rfc3986($parts); return implode('&', $parts); } /** * just uppercases the http method */ public function get_normalized_http_method() { return strtoupper($this->http_method); } /** * Parses {@see http_url} and returns normalized scheme://host/path if non-empty, otherwise return empty string * * @return string */ public function get_normalized_http_url() { if ($this->http_url === '') { return ''; } $parts = parse_url($this->http_url); $port = @$parts['port']; $scheme = $parts['scheme']; $host = $parts['host']; $path = @$parts['path']; $port or $port = ($scheme == 'https') ? '443' : '80'; if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { $host = "$host:$port"; } return "$scheme://$host$path"; } /** * builds a url usable for a GET request */ public function to_url() { $post_data = $this->to_postdata(); $out = $this->get_normalized_http_url(); if ($post_data) { $out .= '?'.$post_data; } return $out; } /** * builds the data one would send in a POST request */ public function to_postdata() { return OAuthUtil::build_http_query($this->parameters); } /** * builds the Authorization: header */ public function to_header() { $out = 'Authorization: OAuth realm=""'; $total = array(); foreach ($this->parameters as $k => $v) { if (substr($k, 0, 5) != "oauth") { continue; } if (is_array($v)) { throw new OAuthException('Arrays not supported in headers'); } $out .= ',' . OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"'; } return $out; } public function __toString() { return $this->to_url(); } public function sign_request($signature_method, $consumer, $token) { $this->set_parameter("oauth_signature_method", $signature_method->get_name(), false); $signature = $this->build_signature($signature_method, $consumer, $token); $this->set_parameter("oauth_signature", $signature, false); } public function build_signature($signature_method, $consumer, $token) { $signature = $signature_method->build_signature($this, $consumer, $token); return $signature; } /** * util function: current timestamp */ private static function generate_timestamp() { return time(); } /** * util function: current nonce */ private static function generate_nonce() { $mt = microtime(); $rand = mt_rand(); return md5($mt.$rand); // md5s look nicer than numbers } } class OAuthServer { protected $timestamp_threshold = 300; // in seconds, five minutes protected $version = 1.0; // hi blaine protected $signature_methods = array(); protected $data_store; function __construct($data_store) { $this->data_store = $data_store; } public function add_signature_method($signature_method) { $this->signature_methods[$signature_method->get_name()] = $signature_method; } // high level functions /** * process a request_token request * returns the request token on success */ public function fetch_request_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); // no token required for the initial token request $token = null; $this->check_signature($request, $consumer, $token); $new_token = $this->data_store->new_request_token($consumer); return $new_token; } /** * process an access_token request * returns the access token on success */ public function fetch_access_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); // requires authorized request token $token = $this->get_token($request, $consumer, "request"); $this->check_signature($request, $consumer, $token); $new_token = $this->data_store->new_access_token($token, $consumer); return $new_token; } /** * verify an api call, checks all the parameters */ public function verify_request(&$request) { global $lastcomputedsignature; $lastcomputedsignature = false; $this->get_version($request); $consumer = $this->get_consumer($request); $token = $this->get_token($request, $consumer, "access"); $this->check_signature($request, $consumer, $token); return array( $consumer, $token ); } // Internals from here /** * version 1 */ private function get_version(&$request) { $version = $request->get_parameter("oauth_version"); if (!$version) { $version = 1.0; } if ($version && $version != $this->version) { throw new OAuthException("OAuth version '$version' not supported"); } return $version; } /** * figure out the signature with some defaults */ private function get_signature_method(&$request) { $signature_method = @ $request->get_parameter("oauth_signature_method"); if (!$signature_method) { $signature_method = "PLAINTEXT"; } if (!in_array($signature_method, array_keys($this->signature_methods))) { throw new OAuthException("Signature method '$signature_method' not supported " . "try one of the following: " . implode(", ", array_keys($this->signature_methods))); } return $this->signature_methods[$signature_method]; } /** * try to find the consumer for the provided request's consumer key */ private function get_consumer(&$request) { $consumer_key = @ $request->get_parameter("oauth_consumer_key"); if (!$consumer_key) { throw new OAuthException("Invalid consumer key"); } $consumer = $this->data_store->lookup_consumer($consumer_key); if (!$consumer) { throw new OAuthException("Invalid consumer"); } return $consumer; } /** * try to find the token for the provided request's token key */ private function get_token(&$request, $consumer, $token_type = "access") { $token_field = @ $request->get_parameter('oauth_token'); if (!$token_field) { return false; } $token = $this->data_store->lookup_token($consumer, $token_type, $token_field); if (!$token) { throw new OAuthException("Invalid $token_type token: $token_field"); } return $token; } /** * all-in-one function to check the signature on a request * should guess the signature method appropriately */ private function check_signature(&$request, $consumer, $token) { // this should probably be in a different method global $lastcomputedsignature; $lastcomputedsignature = false; $timestamp = @ $request->get_parameter('oauth_timestamp'); $nonce = @ $request->get_parameter('oauth_nonce'); $this->check_timestamp($timestamp); $this->check_nonce($consumer, $token, $nonce, $timestamp); $signature_method = $this->get_signature_method($request); $signature = $request->get_parameter('oauth_signature'); $valid_sig = $signature_method->check_signature($request, $consumer, $token, $signature); if (!$valid_sig) { $ex_text = "Invalid signature"; if ($lastcomputedsignature) { $ex_text = $ex_text . " ours= $lastcomputedsignature yours=$signature"; } throw new OAuthException($ex_text); } } /** * check that the timestamp is new enough */ private function check_timestamp($timestamp) { // verify that timestamp is recentish $now = time(); if ($now - $timestamp > $this->timestamp_threshold) { throw new OAuthException("Expired timestamp, yours $timestamp, ours $now"); } } /** * check that the nonce is not repeated */ private function check_nonce($consumer, $token, $nonce, $timestamp) { // verify that the nonce is uniqueish $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp); if ($found) { throw new OAuthException("Nonce already used: $nonce"); } } } class OAuthDataStore { function lookup_consumer($consumer_key) { // implement me } function lookup_token($consumer, $token_type, $token) { // implement me } function lookup_nonce($consumer, $token, $nonce, $timestamp) { // implement me } function new_request_token($consumer) { // return a new token attached to this consumer } function new_access_token($token, $consumer) { // return a new access token attached to this consumer // for the user associated with this token if the request token // is authorized // should also invalidate the request token } } class OAuthUtil { public static function urlencode_rfc3986($input) { if (is_array($input)) { return array_map(array( 'moodle\mod\lti\OAuthUtil', 'urlencode_rfc3986' ), $input); } else { if (is_scalar($input)) { return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode($input))); } else { return ''; } } } // This decode function isn't taking into consideration the above // modifications to the encoding process. However, this method doesn't // seem to be used anywhere so leaving it as is. public static function urldecode_rfc3986($string) { return urldecode($string); } // Utility function for turning the Authorization: header into // parameters, has to do some unescaping // Can filter out any non-oauth parameters if needed (default behaviour) public static function split_header($header, $only_allow_oauth_parameters = true) { $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; $offset = 0; $params = array(); while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { $match = $matches[0]; $header_name = $matches[2][0]; $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { $params[$header_name] = self::urldecode_rfc3986($header_content); } $offset = $match[1] + strlen($match[0]); } if (isset($params['realm'])) { unset($params['realm']); } return $params; } // helper to try to sort out headers for people who aren't running apache public static function get_headers() { if (function_exists('apache_request_headers')) { // we need this to get the actual Authorization: header // because apache tends to tell us it doesn't exist $in = apache_request_headers(); $out = array(); foreach ($in as $key => $value) { $key = str_replace(" ", "-", ucwords(strtolower(str_replace("-", " ", $key)))); $out[$key] = $value; } return $out; } // otherwise we don't have apache and are just going to have to hope // that $_SERVER actually contains what we need $out = array(); foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) == "HTTP_") { // this is chaos, basically it is just there to capitalize the first // letter of every word that is not an initial HTTP and strip HTTP // code from przemek $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5))))); $out[$key] = $value; } } return $out; } // This function takes a input like a=b&a=c&d=e and returns the parsed // parameters like this // array('a' => array('b','c'), 'd' => 'e') public static function parse_parameters($input) { if (!isset($input) || !$input) { return array(); } $pairs = explode('&', $input); $parsed_parameters = array(); foreach ($pairs as $pair) { $split = explode('=', $pair, 2); $parameter = self::urldecode_rfc3986($split[0]); $value = isset($split[1]) ? self::urldecode_rfc3986($split[1]) : ''; if (isset($parsed_parameters[$parameter])) { // We have already recieved parameter(s) with this name, so add to the list // of parameters with this name if (is_scalar($parsed_parameters[$parameter])) { // This is the first duplicate, so transform scalar (string) into an array // so we can add the duplicates $parsed_parameters[$parameter] = array( $parsed_parameters[$parameter] ); } $parsed_parameters[$parameter][] = $value; } else { $parsed_parameters[$parameter] = $value; } } return $parsed_parameters; } public static function build_http_query($params) { if (!$params) { return ''; } // Urlencode both keys and values $keys = self::urlencode_rfc3986(array_keys($params)); $values = self::urlencode_rfc3986(array_values($params)); $params = array_combine($keys, $values); // Parameters are sorted by name, using lexicographical byte value ordering. // Ref: Spec: 9.1.1 (1) uksort($params, 'strcmp'); $pairs = array(); foreach ($params as $parameter => $value) { if (is_array($value)) { // If two or more parameters share the same name, they are sorted by their value // Ref: Spec: 9.1.1 (1) natsort($value); foreach ($value as $duplicate_value) { $pairs[] = $parameter . '=' . $duplicate_value; } } else { $pairs[] = $parameter . '=' . $value; } } // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) // Each name-value pair is separated by an '&' character (ASCII code 38) return implode('&', $pairs); } } coursetools.php000064400000004267152265520610007653 0ustar00. /** * Shows a tabulated view of all the available LTI tools in a given course. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_lti\output\course_tools_page; require_once("../../config.php"); require_once($CFG->dirroot . '/mod/lti/lib.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $id = required_param('id', PARAM_INT); // Course Id. // Access + permissions. $course = get_course($id); require_course_login($course, false); $context = context_course::instance($course->id); if (!has_capability('mod/lti:addpreconfiguredinstance', $context)) { throw new \moodle_exception('nopermissions', 'error', '', get_string('courseexternaltoolsnoviewpermissions', 'mod_lti')); } // Page setup. global $PAGE, $OUTPUT; $pagetitle = get_string('courseexternaltools', 'mod_lti'); $pageurl = new moodle_url('/mod/lti/coursetools.php', ['id' => $course->id]); $PAGE->set_pagelayout('incourse'); $PAGE->set_context($context); $PAGE->set_url($pageurl); $PAGE->set_title($pagetitle); $PAGE->set_heading(format_string($course->fullname, true, ['context' => $context])); $PAGE->add_body_class('limitedwidth'); // Display. echo $OUTPUT->header(); echo $OUTPUT->heading($pagetitle); $renderer = $PAGE->get_renderer('mod_lti'); $coursetoolspage = new course_tools_page($course->id); echo $renderer->render($coursetoolspage); $PAGE->requires->js_call_amd('mod_lti/course_tools_list', 'init'); echo $OUTPUT->footer(); request_tool.php000064400000005104152265520610010006 0ustar00. /** * Submits a request to administrators to add a tool configuration for the requested site. * * @package mod_lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Chris Scribner */ require_once('../../config.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $instanceid = required_param('instanceid', PARAM_INT); $lti = $DB->get_record('lti', array('id' => $instanceid)); $course = $DB->get_record('course', array('id' => $lti->course)); $cm = get_coursemodule_from_instance('lti', $lti->id, $lti->course, false, MUST_EXIST); $context = context_module::instance($cm->id); require_login($course); require_sesskey(); require_capability('mod/lti:requesttooladd', context_course::instance($lti->course)); $baseurl = lti_get_domain_from_url($lti->toolurl); $url = new moodle_url('/mod/lti/request_tool.php', array('instanceid' => $instanceid)); $PAGE->set_url($url); $pagetitle = strip_tags($course->shortname); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('incourse'); echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($lti->name, true, array('context' => $context))); // Add a tool type if one does not exist already. if (!lti_get_tool_by_url_match($lti->toolurl, $lti->course, LTI_TOOL_STATE_ANY)) { // There are no tools (active, pending, or rejected) for the launch URL. Create a new pending tool. $tooltype = new stdClass(); $toolconfig = new stdClass(); $toolconfig->lti_toolurl = lti_get_domain_from_url($lti->toolurl); $toolconfig->lti_typename = $toolconfig->lti_toolurl; lti_add_type($tooltype, $toolconfig); echo get_string('lti_tool_request_added', 'lti'); } else { echo get_string('lti_tool_request_existing', 'lti'); } echo $OUTPUT->footer(); service/basicoutcomes/lang/en/ltiservice_basicoutcomes.php000064400000003034152265520610020175 0ustar00. /** * Strings for component 'ltiservice_basicoutcomes', language 'en' * * @package ltiservice_basicoutcomes * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['allow'] = 'Use this service to accept grades from the tool'; $string['ltiservice_basicoutcomes'] = 'Basic Outcomes'; $string['ltiservice_basicoutcomes_help'] = 'Allow the tool to save and retrieve its grades to a gradebook column associated with each link.'; $string['notallow'] = 'Do not use this service'; $string['pluginname'] = 'Basic Outcomes Service'; $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; $string['privacy:metadata:grade'] = 'The tool\'s grades of the user using the LTI consumer.'; $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; service/basicoutcomes/version.php000064400000002056152265520610013251 0ustar00. /** * Version information for the ltiservice_basicoutcomes service. * * @package ltiservice_basicoutcomes * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_basicoutcomes'; service/basicoutcomes/classes/privacy/provider.php000064400000007512152265520610016532 0ustar00. /** * Privacy Subsystem implementation for ltiservice_basicoutcomes. * * @package ltiservice_basicoutcomes * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_basicoutcomes\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_basicoutcomes. * * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->link_external_location('External LTI provider.', [ 'userid' => 'privacy:metadata:userid', 'grade' => 'privacy:metadata:grade', ], 'privacy:metadata:externalpurpose'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all user data which matches the specified context. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } } service/basicoutcomes/classes/local/service/basicoutcomes.php000064400000006437152265520610020622 0ustar00. /** * This file contains a class definition for the Basic Outcomes service * * @package ltiservice_basicoutcomes * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_basicoutcomes\local\service; defined('MOODLE_INTERNAL') || die(); /** * A service implementing Basic Outcomes. * * @package ltiservice_basicoutcomes * @since Moodle 3.7 * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class basicoutcomes extends \mod_lti\local\ltiservice\service_base { /** Scope for accessing the service */ const SCOPE_BASIC_OUTCOMES = 'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome'; /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'basicoutcomes'; $this->name = 'Basic Outcomes'; } /** * Get the resources for this service. * * @return array */ public function get_resources() { if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new \ltiservice_basicoutcomes\local\resources\basicoutcomes($this); } return $this->resources; } /** * Get the scope(s) permitted for the tool relevant to this service. * * @return array */ public function get_permitted_scopes() { $scopes = array(); if (!isset($this->get_typeconfig()['acceptgrades']) || ($this->get_typeconfig()['acceptgrades'] != LTI_SETTING_NEVER)) { $scopes[] = self::SCOPE_BASIC_OUTCOMES; } return $scopes; } /** * Get the scope(s) permitted for the tool relevant to this service. * * @return array */ public function get_scopes() { return [self::SCOPE_BASIC_OUTCOMES]; } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return [ 'lis_outcome_service_url' => [ 'suffix' => 'bo', 'group' => 'basicoutcome', 'claim' => 'lis_outcome_service_url', 'isarray' => false ], 'lis_result_sourcedid' => [ 'suffix' => 'bo', 'group' => 'basicoutcome', 'claim' => 'lis_result_sourcedid', 'isarray' => false ] ]; } } service/basicoutcomes/classes/local/resources/basicoutcomes.php000064400000004317152265520610021167 0ustar00. /** * This file contains a class definition for the Basic Outcomes resource * * @package ltiservice_basicoutcomes * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_basicoutcomes\local\resources; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing the Basic Outcomes service. * * @package ltiservice_basicoutcomes * @since Moodle 3.7 * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class basicoutcomes extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'Outcomes.LTI1'; $this->template = ''; $this->formats[] = 'application/vnd.ims.lti.v1.outcome+xml'; $this->methods[] = 'POST'; } /** * Get the resource fully qualified endpoint. * * @return string */ public function get_endpoint() { $url = new \moodle_url('/mod/lti/service.php'); return $url->out(false); } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { // Should never be called as the endpoint sends requests to the LTI 1 service endpoint. } } service/readme.txt000064400000002154152265520610010210 0ustar00// 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 . /** * This file contains all necessary code to initiate a tool registration process * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ LTI Service plugins directory ============================== This directory contains LTI service plugins which are discoverable from the Tool Consumer Profile. service/toolproxy/lang/en/ltiservice_toolproxy.php000064400000002134152265520610016633 0ustar00. /** * Strings for component 'ltiservice_toolproxy', language 'en' * * @package ltiservice_toolproxy * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Tool Proxy Service'; $string['privacy:metadata'] = 'The Tool Proxy Service plugin does not store any personal data.'; service/toolproxy/version.php000064400000002247152265520610012472 0ustar00. /** * Version information for the ltiservice_toolproxy service. * * @package ltiservice_toolproxy * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_toolproxy'; $plugin->dependencies = [ 'ltiservice_profile' => 2025040800, ]; service/toolproxy/classes/privacy/provider.php000064400000003013152265520610015741 0ustar00. /** * Privacy Subsystem implementation for ltiservice_toolproxy. * * @package ltiservice_toolproxy * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolproxy\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_toolproxy implementing null_provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } service/toolproxy/classes/local/service/toolproxy.php000064400000003605152265520610017252 0ustar00. /** * This file contains a class definition for the Tool Proxy service * * @package ltiservice_toolproxy * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolproxy\local\service; defined('MOODLE_INTERNAL') || die(); /** * A service implementing the Tool Proxy. * * @package ltiservice_toolproxy * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class toolproxy extends \mod_lti\local\ltiservice\service_base { /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'toolproxy'; $this->name = 'Tool Proxy'; } /** * Get the resources for this service. * * @return array */ public function get_resources() { if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new \ltiservice_toolproxy\local\resources\toolproxy($this); } return $this->resources; } } service/toolproxy/classes/local/resources/toolproxy.php000064400000032017152265520610017623 0ustar00. /** * This file contains a class definition for the Tool Proxy resource * * @package ltiservice_toolproxy * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolproxy\local\resources; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/lti/OAuth.php'); require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; /** * A resource implementing the Tool Proxy. * * @package ltiservice_toolproxy * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class toolproxy extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * * @param ltiservice_toolproxy\local\resources\toolproxy $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolProxy.collection'; $this->template = '/toolproxy'; $this->formats[] = 'application/vnd.ims.lti.v2.toolproxy+json'; $this->methods[] = 'POST'; } /** * Execute the request for this resource. * * @param mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { $ok = $this->check_tool(null, $response->get_request_data()); $ok = $ok && ($this->get_service()->get_tool_proxy()); if ($ok) { $toolproxy = $this->get_service()->get_tool_proxy(); } if (!$ok) { $toolproxy = null; $response->set_code(401); } $tools = array(); // Ensure all required elements are present in the Tool Proxy. if ($ok) { $toolproxyjson = json_decode($response->get_request_data()); $ok = !empty($toolproxyjson); if (!$ok) { debugging('Tool proxy is not properly formed JSON'); } else { $ok = isset($toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code); $ok = $ok && isset($toolproxyjson->security_contract->shared_secret); $ok = $ok && isset($toolproxyjson->tool_profile->resource_handler); if (!$ok) { debugging('One or more missing elements from tool proxy: vendor code, shared secret or resource handlers'); } } } // Check all capabilities requested were offered. if ($ok) { $offeredcapabilities = explode("\n", $toolproxy->capabilityoffered); $resources = $toolproxyjson->tool_profile->resource_handler; $errors = array(); foreach ($resources as $resource) { if (isset($resource->message)) { foreach ($resource->message as $message) { if (!in_array($message->message_type, $offeredcapabilities)) { $errors[] = $message->message_type; } else if (isset($resource->parameter)) { foreach ($message->parameter as $parameter) { if (isset($parameter->variable) && !in_array($parameter->variable, $offeredcapabilities)) { $errors[] = $parameter->variable; } } } } } } if (count($errors) > 0) { $ok = false; debugging('Tool proxy contains capabilities which were not offered: ' . implode(', ', $errors)); } } // Check all services requested were offered (only tool services currently supported). $requestsbasicoutcomes = false; if ($ok && isset($toolproxyjson->security_contract->tool_service)) { $contexts = lti_get_contexts($toolproxyjson); $profileservice = lti_get_service_by_name('profile'); $profileservice->set_tool_proxy($toolproxy); $context = $profileservice->get_service_path() . $profileservice->get_resources()[0]->get_path() . '#'; $offeredservices = explode("\n", $toolproxy->serviceoffered); $services = lti_get_services(); $tpservices = $toolproxyjson->security_contract->tool_service; $errors = array(); foreach ($tpservices as $service) { $fqid = lti_get_fqid($contexts, $service->service); $requestsbasicoutcomes = $requestsbasicoutcomes || (substr($fqid, -13) === 'Outcomes.LTI1'); if (substr($fqid, 0, strlen($context)) !== $context) { $errors[] = $service->service; } else { $id = explode('#', $fqid, 2); $aservice = lti_get_service_by_resource_id($services, $id[1]); $classname = explode('\\', get_class($aservice)); if (empty($aservice) || !in_array($classname[count($classname) - 1], $offeredservices)) { $errors[] = $service->service; } } } if (count($errors) > 0) { $ok = false; debugging('Tool proxy contains services which were not offered: ' . implode(', ', $errors)); } } // Extract all launchable tools from the resource handlers. if ($ok) { $resources = $toolproxyjson->tool_profile->resource_handler; $messagetypes = [ 'basic-lti-launch-request', 'ContentItemSelectionRequest', ]; foreach ($resources as $resource) { $launchable = false; $messages = array(); $tool = new \stdClass(); $iconinfo = null; if (is_array($resource->icon_info)) { $iconinfo = $resource->icon_info[0]; } else { $iconinfo = $resource->icon_info; } if (isset($iconinfo) && isset($iconinfo->default_location) && isset($iconinfo->default_location->path)) { $tool->iconpath = $iconinfo->default_location->path; } foreach ($resource->message as $message) { if (in_array($message->message_type, $messagetypes)) { $launchable = $launchable || ($message->message_type === 'basic-lti-launch-request'); $messages[$message->message_type] = $message; } } if (!$launchable) { continue; } $tool->name = $resource->resource_name->default_value; $tool->messages = $messages; $tools[] = $tool; } $ok = count($tools) > 0; if (!$ok) { debugging('No launchable messages found in tool proxy'); } } // Add tools and custom parameters. if ($ok) { $baseurl = ''; if (isset($toolproxyjson->tool_profile->base_url_choice[0]->default_base_url)) { $baseurl = $toolproxyjson->tool_profile->base_url_choice[0]->default_base_url; } $securebaseurl = ''; if (isset($toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url)) { $securebaseurl = $toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url; } foreach ($tools as $tool) { $messages = $tool->messages; $launchrequest = $messages['basic-lti-launch-request']; $config = new \stdClass(); $config->lti_toolurl = "{$baseurl}{$launchrequest->path}"; $config->lti_typename = $tool->name; $config->lti_coursevisible = 1; $config->lti_forcessl = 0; if (isset($messages['ContentItemSelectionRequest'])) { $contentitemrequest = $messages['ContentItemSelectionRequest']; $config->lti_contentitem = 1; if ($launchrequest->path !== $contentitemrequest->path) { $config->lti_toolurl_ContentItemSelectionRequest = $baseurl . $contentitemrequest->path; } $contentitemcapabilities = implode("\n", $contentitemrequest->enabled_capability); $config->lti_enabledcapability_ContentItemSelectionRequest = $contentitemcapabilities; $contentitemparams = self::lti_extract_parameters($contentitemrequest->parameter); $config->lti_parameter_ContentItemSelectionRequest = $contentitemparams; } $type = new \stdClass(); $type->state = LTI_TOOL_STATE_PENDING; $type->ltiversion = LTI_VERSION_2; $type->toolproxyid = $toolproxy->id; // Ensure gradebook column is created. if ($requestsbasicoutcomes && !in_array('BasicOutcome.url', $launchrequest->enabled_capability)) { $launchrequest->enabled_capability[] = 'BasicOutcome.url'; } if ($requestsbasicoutcomes && !in_array('BasicOutcome.sourcedId', $launchrequest->enabled_capability)) { $launchrequest->enabled_capability[] = 'BasicOutcome.sourcedId'; } $type->enabledcapability = implode("\n", $launchrequest->enabled_capability); $type->parameter = self::lti_extract_parameters($launchrequest->parameter); if (!empty($tool->iconpath)) { $type->icon = "{$baseurl}{$tool->iconpath}"; if (!empty($securebaseurl)) { $type->secureicon = "{$securebaseurl}{$tool->iconpath}"; } } $ok = $ok && (lti_add_type($type, $config) !== false); } if (isset($toolproxyjson->custom)) { lti_set_tool_settings($toolproxyjson->custom, $toolproxy->id); } } if (!empty($toolproxy)) { if ($ok) { // If all went OK accept the tool proxy. $toolproxy->state = LTI_TOOL_PROXY_STATE_ACCEPTED; $toolproxy->toolproxy = $response->get_request_data(); $toolproxy->secret = $toolproxyjson->security_contract->shared_secret; $toolproxy->vendorcode = $toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code; $url = $this->get_endpoint(); $body = <<< EOD { "@context" : "http://purl.imsglobal.org/ctx/lti/v2/ToolProxyId", "@type" : "ToolProxy", "@id" : "{$url}", "tool_proxy_guid" : "{$toolproxy->guid}" } EOD; $response->set_code(201); $response->set_content_type('application/vnd.ims.lti.v2.toolproxy.id+json'); $response->set_body($body); } else { // Otherwise reject the tool proxy. $toolproxy->state = LTI_TOOL_PROXY_STATE_REJECTED; $response->set_code(400); } lti_update_tool_proxy($toolproxy); } else { $response->set_code(400); } } /** * Extracts the message parameters from the tool proxy entry * * @param array $parameters Parameter section of a message * * @return String containing parameters */ private static function lti_extract_parameters($parameters) { $params = array(); foreach ($parameters as $parameter) { if (isset($parameter->variable)) { $value = '$' . $parameter->variable; } else { $value = $parameter->fixed; if (strlen($value) > 0) { $first = substr($value, 0, 1); if (($first == '$') || ($first == '\\')) { $value = '\\' . $value; } } } $params[] = "{$parameter->name}={$value}"; } return implode("\n", $params); } } service/memberships/lang/en/ltiservice_memberships.php000064400000004445152265520610017340 0ustar00. /** * Strings for component 'ltiservice_memberships', language 'en' * * @package ltiservice_memberships * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['allow'] = 'Use this service to retrieve members\' information as per privacy settings'; $string['ltiservice_memberships'] = 'IMS LTI Names and Role Provisioning'; $string['ltiservice_memberships_help'] = 'Allow the tool to retrieve members\' info from the course using the IMS LTI Names and Role Provisioning Service. The privacy settings will apply. For course-level requests these will be based on the tool configuration settings. If you wish to always send such details, do not delegate the choice to teachers. Link-level requests will always use the privacy settings which apply to the link.'; $string['notallow'] = 'Do not use this service'; $string['pluginname'] = 'Names and Role Provisioning LTI Service'; $string['privacy:metadata:email'] = 'The email of the user using the LTI consumer.'; $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; $string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI consumer.'; $string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI consumer.'; $string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.'; $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; $string['privacy:metadata:useridnumber'] = 'The ID number of the user using the LTI consumer'; service/memberships/version.php000064400000002150152265520610012722 0ustar00. /** * Version information for the ltiservice_memberships service. * * @package ltiservice_memberships * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_memberships'; service/memberships/classes/privacy/provider.php000064400000010105152265520610016200 0ustar00. /** * Privacy Subsystem implementation for ltiservice_memberships. * * @package ltiservice_memberships * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_memberships\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_memberships. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->link_external_location('External LTI provider.', [ 'userid' => 'privacy:metadata:userid', 'useridnumber' => 'privacy:metadata:useridnumber', 'fullname' => 'privacy:metadata:fullname', 'firstname' => 'privacy:metadata:firstname', 'lastname' => 'privacy:metadata:lastname', 'email' => 'privacy:metadata:email' ], 'privacy:metadata:externalpurpose'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all user data which matches the specified context. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } } service/memberships/classes/local/service/memberships.php000064400000063655152265520610017763 0ustar00. /** * This file contains a class definition for the Memberships service * * @package ltiservice_memberships * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_memberships\local\service; defined('MOODLE_INTERNAL') || die(); /** * A service implementing Memberships. * * @package ltiservice_memberships * @since Moodle 3.0 * @copyright 2015 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class memberships extends \mod_lti\local\ltiservice\service_base { /** Default prefix for context-level roles */ const CONTEXT_ROLE_PREFIX = 'http://purl.imsglobal.org/vocab/lis/v2/membership#'; /** Context-level role for Instructor */ const CONTEXT_ROLE_INSTRUCTOR = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'; /** Context-level role for Learner */ const CONTEXT_ROLE_LEARNER = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'; /** Capability used to identify Instructors */ const INSTRUCTOR_CAPABILITY = 'moodle/course:manageactivities'; /** Always include field */ const ALWAYS_INCLUDE_FIELD = 1; /** Allow the instructor to decide if included */ const DELEGATE_TO_INSTRUCTOR = 2; /** Instructor chose to include field */ const INSTRUCTOR_INCLUDED = 1; /** Instructor delegated and approved for include */ const INSTRUCTOR_DELEGATE_INCLUDED = array(self::DELEGATE_TO_INSTRUCTOR && self::INSTRUCTOR_INCLUDED); /** Scope for reading membership data */ const SCOPE_MEMBERSHIPS_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'memberships'; $this->name = get_string($this->get_component_id(), $this->get_component_id()); } /** * Get the resources for this service. * * @return array */ public function get_resources() { if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new \ltiservice_memberships\local\resources\contextmemberships($this); $this->resources[] = new \ltiservice_memberships\local\resources\linkmemberships($this); } return $this->resources; } /** * Get the scope(s) permitted for the tool relevant to this service. * * @return array */ public function get_permitted_scopes() { $scopes = array(); $ok = !empty($this->get_type()); if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) && ($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) { $scopes[] = self::SCOPE_MEMBERSHIPS_READ; } return $scopes; } /** * Get the scope(s) defined by this service. * * @return array */ public function get_scopes() { return [self::SCOPE_MEMBERSHIPS_READ]; } /** * Get the JSON for members. * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param \context_course $context Course context * @param string $contextid Course ID * @param object $tool Tool instance object * @param string $role User role requested (empty if none) * @param int $limitfrom Position of first record to be returned * @param int $limitnum Maximum number of records to be returned * @param object $lti LTI instance record * @param \core_availability\info_module $info Conditional availability information * for LTI instance (null if context-level request) * * @return string * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see memberships::get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) */ public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) { global $DB; debugging('get_users_json() has been deprecated, ' . 'please use memberships::get_members_json() instead.', DEBUG_DEVELOPER); $course = $DB->get_record('course', array('id' => $contextid), 'id,shortname,fullname', IGNORE_MISSING); $memberships = new memberships(); $memberships->check_tool($tool->id, null, array(self::SCOPE_MEMBERSHIPS_READ)); $response = new \mod_lti\local\ltiservice\response(); $json = $memberships->get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response); return $json; } /** * Get the JSON for members. * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param \context_course $context Course context * @param \course $course Course * @param string $role User role requested (empty if none) * @param int $limitfrom Position of first record to be returned * @param int $limitnum Maximum number of records to be returned * @param object $lti LTI instance record * @param \core_availability\info_module $info Conditional availability information * for LTI instance (null if context-level request) * @param \mod_lti\local\ltiservice\response $response Response object for the request * * @return string */ public function get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) { $withcapability = ''; $exclude = array(); if (!empty($role)) { if ((strpos($role, 'http://') !== 0) && (strpos($role, 'https://') !== 0)) { $role = self::CONTEXT_ROLE_PREFIX . $role; } if ($role === self::CONTEXT_ROLE_INSTRUCTOR) { $withcapability = self::INSTRUCTOR_CAPABILITY; } else if ($role === self::CONTEXT_ROLE_LEARNER) { $exclude = array_keys(get_enrolled_users($context, self::INSTRUCTOR_CAPABILITY, 0, 'u.id', null, null, null, true)); } } $users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, 0, 0, true); if (($response->get_accept() === 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json') || (($response->get_accept() !== 'application/vnd.ims.lis.v2.membershipcontainer+json') && ($this->get_type()->ltiversion === LTI_VERSION_1P3))) { $json = $this->users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response); } else { $json = $this->users_to_jsonld($resource, $users, $course->id, $exclude, $limitfrom, $limitnum, $lti, $info, $response); } return $json; } /** * Get the JSON-LD representation of the users. * * Note that when a limit is set and the exclude array is not empty, then the number of memberships * returned may be less than the limit. * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param array $users Array of user records * @param string $contextid Course ID * @param array $exclude Array of user records to be excluded from the response * @param int $limitfrom Position of first record to be returned * @param int $limitnum Maximum number of records to be returned * @param object $lti LTI instance record * @param \core_availability\info_module $info Conditional availability information * for LTI instance (null if context-level request) * @param \mod_lti\local\ltiservice\response $response Response object for the request * * @return string */ private function users_to_jsonld($resource, $users, $contextid, $exclude, $limitfrom, $limitnum, $lti, $info, $response) { global $DB; $tool = $this->get_type(); $toolconfig = $this->get_typeconfig(); $arrusers = [ '@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer', '@type' => 'Page', '@id' => $resource->get_endpoint(), ]; $arrusers['pageOf'] = [ '@type' => 'LISMembershipContainer', 'membershipSubject' => [ '@type' => 'Context', 'contextId' => $contextid, 'membership' => [] ] ]; $enabledcapabilities = lti_get_enabled_capabilities($tool); $islti2 = $tool->toolproxyid > 0; $n = 0; $more = false; foreach ($users as $user) { if (in_array($user->id, $exclude)) { continue; } if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { continue; } $n++; if ($limitnum > 0) { if ($n <= $limitfrom) { continue; } if (count($arrusers['pageOf']['membershipSubject']['membership']) >= $limitnum) { $more = true; break; } } $member = new \stdClass(); $member->{"@type" } = 'LISPerson'; $membership = new \stdClass(); $membership->status = 'Active'; $membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true)); $instanceconfig = null; if (!is_null($lti)) { $instanceconfig = lti_get_type_config_from_instance($lti->id); } $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, ['name' => 'sendname', 'email' => 'sendemailaddr']); $includedcapabilities = [ 'User.id' => ['type' => 'id', 'member.field' => 'userId', 'source.value' => $user->id], 'Person.sourcedId' => ['type' => 'id', 'member.field' => 'sourcedId', 'source.value' => format_string($user->idnumber)], 'Person.name.full' => ['type' => 'name', 'member.field' => 'name', 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 'Person.name.given' => ['type' => 'name', 'member.field' => 'givenName', 'source.value' => format_string($user->firstname)], 'Person.name.family' => ['type' => 'name', 'member.field' => 'familyName', 'source.value' => format_string($user->lastname)], 'Person.email.primary' => ['type' => 'email', 'member.field' => 'email', 'source.value' => format_string($user->email)], 'User.username' => ['type' => 'name', 'member.field' => 'ext_user_username', 'source.value' => format_string($user->username)] ]; if (!is_null($lti)) { $message = new \stdClass(); $message->message_type = 'basic-lti-launch-request'; $conditions = array('courseid' => $contextid, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id); if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { $message->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, $user->id, $lti->servicesalt, $lti->typeid)); // Not per specification but added to comply with earlier version of the service. $member->resultSourcedId = $message->lis_result_sourcedid; } $membership->message = [$message]; } foreach ($includedcapabilities as $capabilityname => $capability) { if ($islti2) { if (in_array($capabilityname, $enabledcapabilities)) { $member->{$capability['member.field']} = $capability['source.value']; } } else { if (($capability['type'] === 'id') || ($capability['type'] === 'name' && $isallowedlticonfig['name']) || ($capability['type'] === 'email' && $isallowedlticonfig['email'])) { $member->{$capability['member.field']} = $capability['source.value']; } } } $membership->member = $member; $arrusers['pageOf']['membershipSubject']['membership'][] = $membership; } if ($more) { $nextlimitfrom = $limitfrom + $limitnum; $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; if (!is_null($lti)) { $nextpage .= "&rlid={$lti->id}"; } $arrusers['nextPage'] = $nextpage; } $response->set_content_type('application/vnd.ims.lis.v2.membershipcontainer+json'); return json_encode($arrusers); } /** * Get the NRP service JSON representation of the users. * * Note that when a limit is set and the exclude array is not empty, then the number of memberships * returned may be less than the limit. * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param array $users Array of user records * @param \course $course Course * @param array $exclude Array of user records to be excluded from the response * @param int $limitfrom Position of first record to be returned * @param int $limitnum Maximum number of records to be returned * @param object $lti LTI instance record * @param \core_availability\info_module $info Conditional availability information for LTI instance * @param \mod_lti\local\ltiservice\response $response Response object for the request * * @return string */ private function users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response) { global $DB, $CFG; $tool = $this->get_type(); $toolconfig = $this->get_typeconfig(); $context = new \stdClass(); $context->id = $course->id; $context->label = trim(html_to_text($course->shortname, 0)); $context->title = trim(html_to_text($course->fullname, 0)); $arrusers = [ 'id' => $resource->get_endpoint(), 'context' => $context, 'members' => [] ]; $islti2 = $tool->toolproxyid > 0; $n = 0; $more = false; foreach ($users as $user) { if (in_array($user->id, $exclude)) { continue; } if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { continue; } $n++; if ($limitnum > 0) { if ($n <= $limitfrom) { continue; } if (count($arrusers['members']) >= $limitnum) { $more = true; break; } } $member = new \stdClass(); $member->status = 'Active'; $member->roles = explode(',', lti_get_ims_role($user->id, null, $course->id, true)); $instanceconfig = null; if (!is_null($lti)) { $instanceconfig = lti_get_type_config_from_instance($lti->id); } if (!$islti2) { $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, ['name' => 'sendname', 'givenname' => 'sendname', 'familyname' => 'sendname', 'email' => 'sendemailaddr']); } else { $isallowedlticonfig = self::is_allowed_capability_set($tool, ['name' => 'Person.name.full', 'givenname' => 'Person.name.given', 'familyname' => 'Person.name.family', 'email' => 'Person.email.primary']); } $includedcapabilities = [ 'User.id' => ['type' => 'id', 'member.field' => 'user_id', 'source.value' => $user->id], 'Person.sourcedId' => ['type' => 'id', 'member.field' => 'lis_person_sourcedid', 'source.value' => format_string($user->idnumber)], 'Person.name.full' => ['type' => 'name', 'member.field' => 'name', 'source.value' => format_string("{$user->firstname} {$user->lastname}")], 'Person.name.given' => ['type' => 'givenname', 'member.field' => 'given_name', 'source.value' => format_string($user->firstname)], 'Person.name.family' => ['type' => 'familyname', 'member.field' => 'family_name', 'source.value' => format_string($user->lastname)], 'Person.email.primary' => ['type' => 'email', 'member.field' => 'email', 'source.value' => format_string($user->email)], 'User.username' => ['type' => 'name', 'member.field' => 'ext_user_username', 'source.value' => format_string($user->username)], ]; if (!is_null($lti)) { $message = new \stdClass(); $message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} = 'LtiResourceLinkRequest'; $conditions = array('courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id); if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { $basicoutcome = new \stdClass(); $basicoutcome->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, $user->id, $lti->servicesalt, $lti->typeid)); // Add outcome service URL. $serviceurl = new \moodle_url('/mod/lti/service.php'); $serviceurl = $serviceurl->out(); $forcessl = false; if (!empty($CFG->mod_lti_forcessl)) { $forcessl = true; } if ((isset($toolconfig['forcessl']) && ($toolconfig['forcessl'] == '1')) or $forcessl) { $serviceurl = lti_ensure_url_is_https($serviceurl); } $basicoutcome->lis_outcome_service_url = $serviceurl; $message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'} = $basicoutcome; } $member->message = [$message]; } foreach ($includedcapabilities as $capabilityname => $capability) { if (($capability['type'] === 'id') || $isallowedlticonfig[$capability['type']]) { $member->{$capability['member.field']} = $capability['source.value']; } } $arrusers['members'][] = $member; } if ($more) { $nextlimitfrom = $limitfrom + $limitnum; $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}"; if (!is_null($lti)) { $nextpage .= "&rlid={$lti->id}"; } $response->add_additional_header("Link: <{$nextpage}>; rel=\"next\""); } $response->set_content_type('application/vnd.ims.lti-nrps.v2.membershipcontainer+json'); return json_encode($arrusers); } /** * Determines whether a user attribute may be used as part of LTI membership * @param array $toolconfig Tool config * @param object $instanceconfig Tool instance config * @param array $fields Set of fields to return if allowed or not * @return array Verification which associates an attribute with a boolean (allowed or not) */ private static function is_allowed_field_set($toolconfig, $instanceconfig, $fields) { $isallowedstate = []; foreach ($fields as $key => $field) { $allowed = isset($toolconfig[$field]) && (self::ALWAYS_INCLUDE_FIELD == $toolconfig[$field]); if (!$allowed && isset($toolconfig[$field]) && (self::DELEGATE_TO_INSTRUCTOR == $toolconfig[$field]) && !is_null($instanceconfig)) { $allowed = isset($instanceconfig->{"lti_{$field}"}) && ($instanceconfig->{"lti_{$field}"} == self::INSTRUCTOR_INCLUDED); } $isallowedstate[$key] = $allowed; } return $isallowedstate; } /** * Adds form elements for membership add/edit page. * * @param \MoodleQuickForm $mform */ public function get_configuration_options(&$mform) { $elementname = $this->get_component_id(); $options = [ get_string('notallow', $this->get_component_id()), get_string('allow', $this->get_component_id()) ]; $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options); $mform->setType($elementname, 'int'); $mform->setDefault($elementname, 0); $mform->addHelpButton($elementname, $elementname, $this->get_component_id()); } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid The course id. * @param string $user The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration * and not return parameters for services not used. * * @return array of key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { global $COURSE; $launchparameters = array(); $tool = lti_get_type_type_config($typeid); if (isset($tool->{$this->get_component_id()})) { if ($tool->{$this->get_component_id()} == parent::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) { $launchparameters['context_memberships_url'] = '$ToolProxyBinding.memberships.url'; $launchparameters['context_memberships_v2_url'] = '$ToolProxyBinding.memberships.url'; $launchparameters['context_memberships_versions'] = '1.0,2.0'; } } return $launchparameters; } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return [ 'custom_context_memberships_v2_url' => [ 'suffix' => 'nrps', 'group' => 'namesroleservice', 'claim' => 'context_memberships_url', 'isarray' => false ], 'custom_context_memberships_versions' => [ 'suffix' => 'nrps', 'group' => 'namesroleservice', 'claim' => 'service_versions', 'isarray' => true ] ]; } } service/memberships/classes/local/resources/contextmemberships.php000064400000012566152265520610021735 0ustar00. /** * This file contains a class definition for the Context Memberships resource * * @package ltiservice_memberships * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_memberships\local\resources; use mod_lti\local\ltiservice\resource_base; use ltiservice_memberships\local\service\memberships; use core_availability\info_module; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing Context Memberships. * * @package ltiservice_memberships * @since Moodle 3.0 * @copyright 2015 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextmemberships extends resource_base { /** * Class constructor. * * @param \ltiservice_memberships\local\service\memberships $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolProxyBindingMemberships'; $this->template = '/{context_type}/{context_id}/bindings/{tool_code}/memberships'; $this->variables[] = 'ToolProxyBinding.memberships.url'; $this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+json'; $this->formats[] = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json'; $this->methods[] = self::HTTP_GET; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB; $params = $this->parse_template(); $role = optional_param('role', '', PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); $linkid = optional_param('rlid', '', PARAM_TEXT); $lti = null; $modinfo = null; if ($limitnum <= 0) { $limitfrom = 0; } try { if (!$this->check_tool($params['tool_code'], $response->get_request_data(), array(memberships::SCOPE_MEMBERSHIPS_READ))) { throw new \Exception(null, 401); } if (!($course = $DB->get_record('course', array('id' => $params['context_id']), 'id,shortname,fullname', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$params['context_id']} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($params['tool_code'], $course->id)) { throw new \Exception(null, 404); } if (!($context = \context_course::instance($course->id))) { throw new \Exception("Not Found: Course instance {$course->id} doesn't exist", 404); } if (!empty($linkid)) { if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) { throw new \Exception("Not Found: LTI link {$linkid} doesn't exist", 404); } $modinfo = get_fast_modinfo($course); $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST); $cm = $modinfo->get_cm($cm->id); $modinfo = new info_module($cm); if ($modinfo->is_available_for_all()) { $modinfo = null; } } $json = $this->get_service()->get_members_json($this, $context, $course, $role, $limitfrom, $limitnum, $lti, $modinfo, $response); $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $DB; if (strpos($value, '$ToolProxyBinding.memberships.url') !== false) { if ($COURSE->id === SITEID) { $this->params['context_type'] = 'Group'; } else { $this->params['context_type'] = 'CourseSection'; } $this->params['context_id'] = $COURSE->id; if ($tool = $this->get_service()->get_type()) { $this->params['tool_code'] = $tool->id; } $value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value); } return $value; } } service/memberships/classes/local/resources/linkmemberships.php000064400000012252152265520610021176 0ustar00. /** * This file contains a class definition for the Link Memberships resource * * @package ltiservice_memberships * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_memberships\local\resources; use mod_lti\local\ltiservice\resource_base; use ltiservice_memberships\local\service\memberships; use core_availability\info_module; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing Link Memberships. * The link membership is no longer defined in the published * version of the LTI specification. It is replaced by the * rlid parameter in the context membership URL. * * @package ltiservice_memberships * @since Moodle 3.0 * @copyright 2015 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class linkmemberships extends resource_base { /** * Class constructor. * * @param \ltiservice_memberships\local\service\memberships $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LtiLinkMemberships'; $this->template = '/links/{link_id}/memberships'; $this->variables[] = 'LtiLink.memberships.url'; $this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+json'; $this->methods[] = 'GET'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB; $params = $this->parse_template(); $linkid = $params['link_id']; $role = optional_param('role', '', PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); if ($limitnum <= 0) { $limitfrom = 0; } if (empty($linkid)) { $response->set_code(404); return; } if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) { $response->set_code(404); return; } if (!$this->check_tool($lti->typeid, $response->get_request_data(), array(memberships::SCOPE_MEMBERSHIPS_READ))) { $response->set_code(403); return; } if (!($course = $DB->get_record('course', array('id' => $lti->course), 'id', IGNORE_MISSING))) { $response->set_code(404); return; } if (!($context = \context_course::instance($lti->course))) { $response->set_code(404); return; } $modinfo = get_fast_modinfo($course); $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST); $cm = $modinfo->get_cm($cm->id); $info = new info_module($cm); if ($info->is_available_for_all()) { $info = null; } $json = $this->get_service()->get_members_json($this, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response); $response->set_content_type($this->formats[0]); $response->set_body($json); } /** * get permissions from the config of the tool for that resource * * @param string $typeid * * @return array with the permissions related to this resource by the $lti_type or null if none. */ public function get_permissions($typeid) { $tool = lti_get_type_type_config($typeid); if ($tool->memberships == '1') { return array('ToolProxyBinding.memberships.url:get'); } else { return array(); } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { if (strpos($value, '$LtiLink.memberships.url') !== false) { $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $this->params['link_id'] = $cm->instance; } $value = str_replace('$LtiLink.memberships.url', parent::get_endpoint(), $value); } return $value; } } service/memberships/tests/privacy/provider_test.php000064400000003415152265520610016752 0ustar00. /** * Unit tests for ltiservice_memberships privacy provider. * * @package ltiservice_memberships * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_memberships\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; /** * Unit tests for ltiservice_memberships privacy provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $contextlist = \ltiservice_memberships\privacy\provider::get_contexts_for_userid($user->id); $this->assertEmpty($contextlist); } } service/gradebookservices/db/tasks.php000064400000002430152265520610014133 0ustar00. /** * This file defines tasks performed by the plugin. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // List of tasks. $tasks = array( array( 'classname' => 'ltiservice_gradebookservices\task\cleanup_task', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ) ); service/gradebookservices/db/upgrade.php000064400000006107152265520610014442 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines tasks performed by the plugin. * * @package ltiservice_gradebookservices * @copyright 2020 Cengage Learning http://www.cengage.com * @author Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * xmldb_ltiservice_gradebookservices_upgrade is the function that upgrades * the gradebook lti service subplugin database when is needed. * * This function is automatically called when version number in * version.php changes. * * @param int $oldversion New old version number. * * @return boolean */ function xmldb_ltiservice_gradebookservices_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } service/gradebookservices/db/install.xml000064400000004430152265520610014467 0ustar00
service/gradebookservices/lang/en/ltiservice_gradebookservices.php000064400000005156152265520610021706 0ustar00. /** * Strings for component 'ltiservice_gradebookservices', language 'en' * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['alwaysgs'] = 'Use this service for grade sync and column management '; $string['grade_synchronization'] = 'IMS LTI Assignment and Grade Services'; $string['grade_synchronization_help'] = 'Whether to use the IMS LTI Assignment and Grade Services to synchronise grades instead of the Basic Outcomes service. * **Do not use this service** - Basic Outcomes features and configuration will be used * **Use this service for grade sync only** - The service will populate the grades in an already existing gradebook column, but it will not be able to create new columns * **Use this service for grade sync and column management** - The service will be able to create and update gradebook columns and manage the grades.'; $string['ltiservice_gradebookservices'] = 'IMS LTI Assignment and Grade Services'; $string['modulename'] = 'LTI Grades'; $string['nevergs'] = 'Do not use this service'; $string['partialgs'] = 'Use this service for grade sync only'; $string['pluginname'] = 'LTI Assignment and Grade Services'; $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; $string['privacy:metadata:feedback'] = 'The feedback the user received for this LTI activity.'; $string['privacy:metadata:grade'] = 'The grade the user received in Moodle for this LTI activity.'; $string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.'; $string['privacy:metadata:timemodified'] = 'The last time the grade was updated'; $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; $string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup'; service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php000064400000013550152265520610027707 0ustar00. /** * This file contains the class for restore of this gradebookservices plugin * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * Provides the information to backup gradebookservices lineitems * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ltiservice_gradebookservices_subplugin extends backup_subplugin { /** TypeId contained in DB but is invalid */ const NONVALIDTYPEID = 0; /** * Returns the subplugin information to attach to submission element * @return backup_subplugin_element */ protected function define_lti_subplugin_structure() { global $DB; // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); // The gbs entries related with this element. $lineitems = new backup_nested_element('lineitems'); $lineitem = new backup_nested_element('lineitem', array('id'), array( 'gradeitemid', 'courseid', 'toolproxyid', 'typeid', 'baseurl', 'ltilinkid', 'resourceid', 'tag', 'vendorcode', 'guid', 'subreviewurl', 'subreviewparams' ) ); // Build the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($lineitems); $lineitems->add_child($lineitem); // We need to know the actual activity tool or toolproxy. // If and activity is assigned to a type that doesn't exists we don't want to backup any related lineitems.`` // Default to invalid condition. $typeid = 0; $toolproxyid = '0'; /* cache parent property to account for missing PHPDoc type specification */ /** @var backup_activity_task $activitytask */ $activitytask = $this->task; $activityid = $activitytask->get_activityid(); $activitycourseid = $activitytask->get_courseid(); $lti = $DB->get_record('lti', ['id' => $activityid], 'typeid, toolurl, securetoolurl'); $ltitype = $DB->get_record('lti_types', ['id' => $lti->typeid], 'toolproxyid, baseurl'); if ($ltitype) { $typeid = $lti->typeid; $toolproxyid = $ltitype->toolproxyid; } else if ($lti->typeid == self::NONVALIDTYPEID) { // This activity comes from an old backup. // 1. Let's check if the activity is coupled. If so, find the values in the GBS element. $gbsrecord = $DB->get_record('ltiservice_gradebookservices', ['ltilinkid' => $activityid], 'typeid,toolproxyid,baseurl'); if ($gbsrecord) { $typeid = $gbsrecord->typeid; $toolproxyid = $gbsrecord->toolproxyid; } else { // 2. If it is uncoupled... we will need to guess the right activity typeid // Guess the typeid for the activity. $tool = lti_get_tool_by_url_match($lti->toolurl, $activitycourseid); if (!$tool) { $tool = lti_get_tool_by_url_match($lti->securetoolurl, $activitycourseid); } if ($tool) { $alttypeid = $tool->id; // If we have a valid typeid then get types again. if ($alttypeid != self::NONVALIDTYPEID) { $ltitype = $DB->get_record('lti_types', ['id' => $alttypeid], 'toolproxyid, baseurl'); $toolproxyid = $ltitype->toolproxyid; } } } } // Define sources. if ($toolproxyid != null) { $lineitemssql = "SELECT l.*, t.vendorcode as vendorcode, t.guid as guid FROM {ltiservice_gradebookservices} l INNER JOIN {lti_tool_proxies} t ON (t.id = l.toolproxyid) WHERE l.courseid = ? AND l.toolproxyid = ? AND l.typeid is null"; $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($toolproxyid)]; } else { $lineitemssql = "SELECT l.*, null as vendorcode, null as guid FROM {ltiservice_gradebookservices} l WHERE l.courseid = ? AND l.typeid = ? AND l.toolproxyid is null"; $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($typeid)]; } $lineitem->set_source_sql($lineitemssql, $lineitemsparams); return $subplugin; } } service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php000064400000024663152265520610030134 0ustar00. /** * This file contains the class for restore of this gradebookservices plugin * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/lti/locallib.php'); /** * Restore subplugin class. * * Provides the necessary information * needed to restore the lineitems related with the lti activity (coupled), * and all the uncoupled ones from the course. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ltiservice_gradebookservices_subplugin extends restore_subplugin { /** * Returns the subplugin structure to attach to the XML element. * * @return restore_path_element[] array of elements to be processed on restore. */ protected function define_lti_subplugin_structure() { $paths = array(); $elename = $this->get_namefor('lineitem'); $elepath = $this->get_pathfor('/lineitems/lineitem'); $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one lineitem * * @param mixed $data * @return void */ public function process_ltiservice_gradebookservices_lineitem($data) { global $DB; $data = (object)$data; // The coupled lineitems are restored as any other grade item // so we will only create the entry in the ltiservice_gradebookservices table. // We will try to find a valid toolproxy in the system. // If it has been found before... we use it. /* cache parent property to account for missing PHPDoc type specification */ /** @var backup_activity_task $activitytask */ $activitytask = $this->task; $courseid = $activitytask->get_courseid(); if ($data->typeid != null) { if ($ltitypeid = $this->get_mappingid('ltitype', $data->typeid)) { $newtypeid = $ltitypeid; } else { // If not, then we will call our own function to find it. $newtypeid = $this->find_typeid($data, $courseid); } } else { $newtypeid = null; } if ($data->toolproxyid != null) { $ltitoolproxy = $this->get_mappingid('ltitoolproxy', $data->toolproxyid); if ($ltitoolproxy && $ltitoolproxy != 0) { $newtoolproxyid = $ltitoolproxy; } else { // If not, then we will call our own function to find it. $newtoolproxyid = $this->find_proxy_id($data); } } else { $newtoolproxyid = null; } if ($data->ltilinkid != null) { if ($data->ltilinkid != $this->get_old_parentid('lti')) { // This is a linked item, but not for the current lti link, so skip it. return; } $ltilinkid = $this->get_new_parentid('lti'); } else { $ltilinkid = null; } $resourceid = null; if (property_exists( $data, 'resourceid' )) { $resourceid = $data->resourceid; } // If this has not been restored before. if ($this->get_mappingid('gbsgradeitemrestored', $data->id, 0) == 0) { $newgbsid = $DB->insert_record('ltiservice_gradebookservices', (object) array( 'gradeitemid' => 0, 'courseid' => $courseid, 'toolproxyid' => $newtoolproxyid, 'ltilinkid' => $ltilinkid, 'typeid' => $newtypeid, 'baseurl' => $data->baseurl, 'resourceid' => $resourceid, 'tag' => $data->tag, 'subreviewparams' => $data->subreviewparams ?? '', 'subreviewurl' => $data->subreviewurl ?? '' )); $this->set_mapping('gbsgradeitemoldid', $newgbsid, $data->gradeitemid); $this->set_mapping('gbsgradeitemrestored', $data->id, $data->id); } } /** * If the toolproxy is not in the mapping (or it is 0) * we try to find the toolproxyid. * If none is found, then we set it to 0. * * @param mixed $data * @return integer $newtoolproxyid */ private function find_proxy_id($data) { global $DB; $newtoolproxyid = 0; $oldtoolproxyguid = $data->guid; $oldtoolproxyvendor = $data->vendorcode; $dbtoolproxyjsonparams = array('guid' => $oldtoolproxyguid, 'vendorcode' => $oldtoolproxyvendor); $dbtoolproxy = $DB->get_field('lti_tool_proxies', 'id', $dbtoolproxyjsonparams, IGNORE_MISSING); if ($dbtoolproxy) { $newtoolproxyid = $dbtoolproxy; } return $newtoolproxyid; } /** * If the typeid is not in the mapping or it is 0, (it should be most of the times) * we will try to find the better typeid that matches with the lineitem. * If none is found, then we set it to 0. * * @param stdClass $data * @param int $courseid * @return int The item type id */ private function find_typeid($data, $courseid) { global $DB; $newtypeid = 0; $oldtypeid = $data->typeid; // 1. Find a type with the same id in the same course. $dbtypeidparameter = array('id' => $oldtypeid, 'course' => $courseid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id AND course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 2. Find a site type for all the courses (course == 1), but with the same id. $dbtypeidparameter = array('id' => $oldtypeid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id AND course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 3. Find a type with the same baseurl in the actual site. $dbtypeidparameter = array('course' => $courseid, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } else { // 4. Find a site type for all the courses (course == 1) with the same baseurl. $dbtypeidparameter = array('course' => 1, 'baseurl' => $data->baseurl); $dbtype = $DB->get_field_select('lti_types', 'id', "course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl", $dbtypeidparameter); if ($dbtype) { $newtypeid = $dbtype; } } } } return $newtypeid; } /** * We call the after_restore_lti to update the grade_items id's that we didn't know in the moment of creating * the gradebookservices rows. */ protected function after_restore_lti() { global $DB; $activitytask = $this->task; $courseid = $activitytask->get_courseid(); $gbstoupdate = $DB->get_records('ltiservice_gradebookservices', array('gradeitemid' => 0, 'courseid' => $courseid)); foreach ($gbstoupdate as $gbs) { $oldgradeitemid = $this->get_mappingid('gbsgradeitemoldid', $gbs->id, 0); $newgradeitemid = $this->get_mappingid('grade_item', $oldgradeitemid, 0); if ($newgradeitemid > 0) { $gbs->gradeitemid = $newgradeitemid; if (!isset($gbs->resourceid)) { // Before 3.9 resourceid was stored in grade_item->idnumber. $gbs->resourceid = $DB->get_field_select('grade_items', 'idnumber', "id=:id", ['id' => $newgradeitemid]); } $DB->update_record('ltiservice_gradebookservices', $gbs); } } // Pre 3.9 backups did not include a gradebookservices record. Adding one here if missing for the restored instance. $gi = $DB->get_record('grade_items', array('itemtype' => 'mod', 'itemmodule' => 'lti', 'courseid' => $courseid, 'iteminstance' => $this->task->get_activityid())); if ($gi) { $gbs = $DB->get_records('ltiservice_gradebookservices', ['gradeitemid' => $gi->id]); if (empty($gbs)) { // The currently restored LTI link has a grade item but no gbs, so let's create a gbs entry. if ($instance = $DB->get_record('lti', array('id' => $gi->iteminstance))) { if ($tool = lti_get_instance_type($instance)) { $DB->insert_record('ltiservice_gradebookservices', (object) array( 'gradeitemid' => $gi->id, 'courseid' => $courseid, 'toolproxyid' => $tool->toolproxyid, 'ltilinkid' => $gi->iteminstance, 'typeid' => $tool->id, 'baseurl' => $tool->baseurl, 'resourceid' => $gi->idnumber )); } } } } } } service/gradebookservices/version.php000064400000002217152265520610014111 0ustar00. /** * Version information for the ltiservice_gradebookservices service. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_gradebookservices'; service/gradebookservices/classes/privacy/provider.php000064400000010044152265520610017365 0ustar00. /** * Privacy Subsystem implementation for ltiservice_gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_gradebookservices. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->link_external_location('External LTI provider.', [ 'userid' => 'privacy:metadata:userid', 'grade' => 'privacy:metadata:grade', 'maxgrade' => 'privacy:metadata:maxgrade', 'feedback' => 'privacy:metadata:feedback', 'timemodified' => 'privacy:metadata:timemodified' ], 'privacy:metadata:externalpurpose'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all user data which matches the specified context. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } } service/gradebookservices/classes/local/service/gradebookservices.php000064400000111103152265520610022307 0ustar00. /** * This file contains a class definition for the LTI Gradebook Services * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\service; use ltiservice_gradebookservices\local\resources\lineitem; use ltiservice_gradebookservices\local\resources\lineitems; use ltiservice_gradebookservices\local\resources\results; use ltiservice_gradebookservices\local\resources\scores; use mod_lti\local\ltiservice\resource_base; use mod_lti\local\ltiservice\service_base; use moodle_url; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * A service implementing LTI Gradebook Services. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradebookservices extends service_base { /** Read-only access to Gradebook services */ const GRADEBOOKSERVICES_READ = 1; /** Full access to Gradebook services */ const GRADEBOOKSERVICES_FULL = 2; /** Scope for full access to Lineitem service */ const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; /** Scope for full access to Lineitem service */ const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; /** Scope for access to Result service */ const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** Scope for access to Score service */ const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'gradebookservices'; $this->name = get_string($this->get_component_id(), $this->get_component_id()); } /** * Get the resources for this service. * * @return resource_base[] */ public function get_resources() { // The containers should be ordered in the array after their elements. // Lineitems should be after lineitem. if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new lineitem($this); $this->resources[] = new lineitems($this); $this->resources[] = new results($this); $this->resources[] = new scores($this); } return $this->resources; } /** * Get the scope(s) permitted for this service. * * @return array */ public function get_permitted_scopes() { $scopes = array(); $ok = !empty($this->get_type()); if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) { $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ; $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE; if ($setting == self::GRADEBOOKSERVICES_FULL) { $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM; } } } return $scopes; } /** * Get the scopes defined by this service. * * @return array */ public function get_scopes() { return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ, self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM]; } /** * Adds form elements for gradebook sync add/edit page. * * @param \MoodleQuickForm $mform Moodle quickform object definition */ public function get_configuration_options(&$mform) { $selectelementname = 'ltiservice_gradesynchronization'; $identifier = 'grade_synchronization'; $options = [ get_string('nevergs', $this->get_component_id()), get_string('partialgs', $this->get_component_id()), get_string('alwaysgs', $this->get_component_id()) ]; $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options); $mform->setType($selectelementname, 'int'); $mform->setDefault($selectelementname, 0); $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id()); } /** * For submission review, if there is a dedicated URL, use it as the target link. * * @param string $messagetype message type for this launch * @param string $targetlinkuri current target link uri * @param string|null $customstr concatenated list of custom parameters * @param int $courseid * @param null|object $lti LTI Instance. * * @return array containing the target link URL and the custom params string to use. */ public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid, ?object $lti = null): array { global $DB; if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) { $conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id); $coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions); if (count($coupledlineitems) == 1) { $item = reset($coupledlineitems); $url = $item->subreviewurl; $subreviewparams = $item->subreviewparams; if (!empty($url) && $url != 'DEFAULT') { $targetlinkuri = $url; } if (!empty($subreviewparams)) { if (!empty($customstr)) { $customstr .= "\n{$subreviewparams}"; } else { $customstr = $subreviewparams; } } } } return [$targetlinkuri, $customstr]; } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return [ 'custom_gradebookservices_scope' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scope', 'isarray' => true ], 'custom_lineitems_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitems', 'isarray' => false ], 'custom_lineitem_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitem', 'isarray' => false ], 'custom_results_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'results', 'isarray' => false ], 'custom_result_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'result', 'isarray' => false ], 'custom_scores_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scores', 'isarray' => false ], 'custom_score_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'score', 'isarray' => false ] ]; } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid the course id. * @param object $user The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration * and not return parameters for services not used. * * @return array of key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { global $DB; $launchparameters = array(); $this->set_type(lti_get_type($typeid)); $this->set_typeconfig(lti_get_type_config($typeid)); // Only inject parameters if the service is enabled for this tool. if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) { if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ || $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) { // Check for used in context is only needed because there is no explicit site tool - course relation. if ($this->is_allowed_in_context($typeid, $courseid)) { $id = null; if (!is_null($modlti)) { $conditions = array('courseid' => $courseid, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $modlti); $coupledlineitems = $DB->get_records('grade_items', $conditions); $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti); $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs); // If a link has more that one attached grade items, per spec we do not populate line item url. if (count($lineitemsgbs) == 1) { $id = reset($lineitemsgbs)->gradeitemid; } if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) { $coupledid = reset($coupledlineitems)->id; if (!is_null($id) && $id != $coupledid) { $id = null; } else { $id = $coupledid; } } } $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes()); $launchparameters['lineitems_url'] = '$LineItems.url'; if (!is_null($id)) { $launchparameters['lineitem_url'] = '$LineItem.url'; } } } } return $launchparameters; } /** * Fetch the lineitem instances. * * @param string $courseid ID of course * @param string $resourceid Resource identifier used for filtering, may be null * @param string $ltilinkid Resource Link identifier used for filtering, may be null * @param string $tag * @param int $limitfrom Offset for the first line item to include in a paged set * @param int $limitnum Maximum number of line items to include in the paged set * @param string $typeid * * @return array * @throws \Exception */ public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) { global $DB; // Select all lti potential linetiems in site. $params = array('courseid' => $courseid); $sql = "SELECT i.* FROM {grade_items} i WHERE (i.courseid = :courseid) ORDER BY i.id"; $lineitems = $DB->get_records_sql($sql, $params); // For each one, check the gbs id, and check that toolproxy matches. If so, add the // tag to the result and add it to a final results array. $lineitemstoreturn = array(); $lineitemsandtotalcount = array(); if ($lineitems) { foreach ($lineitems as $lineitem) { $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id); if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag)) && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid)) && (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) { if (is_null($typeid)) { if ($this->get_tool_proxy()->id == $gbs->toolproxyid) { array_push($lineitemstoreturn, $lineitem); } } else { if ($typeid == $gbs->typeid) { array_push($lineitemstoreturn, $lineitem); } } } else if (($lineitem->itemtype == 'mod' && $lineitem->itemmodule == 'lti' && !isset($resourceid) && !isset($tag) && (!isset($ltilinkid) || (isset($ltilinkid) && $lineitem->iteminstance == $ltilinkid)))) { // We will need to check if the activity related belongs to our tool proxy. $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); if (($ltiactivity) && (isset($ltiactivity->typeid))) { if ($ltiactivity->typeid != 0) { $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); } else { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); } } if (is_null($typeid)) { if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) { array_push($lineitemstoreturn, $lineitem); } } else { if (($tool) && ($tool->id == $typeid)) { array_push($lineitemstoreturn, $lineitem); } } } } } $lineitemsandtotalcount = array(); array_push($lineitemsandtotalcount, count($lineitemstoreturn)); // Return the right array based in the paging parameters limit and from. if (($limitnum) && ($limitnum > 0)) { $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum); } array_push($lineitemsandtotalcount, $lineitemstoreturn); } return $lineitemsandtotalcount; } /** * Fetch a lineitem instance. * * Returns the lineitem instance if found, otherwise false. * * @param string $courseid ID of course * @param string $itemid ID of lineitem * @param string $typeid * * @return \ltiservice_gradebookservices\local\resources\lineitem|bool */ public function get_lineitem($courseid, $itemid, $typeid) { global $DB, $CFG; require_once($CFG->libdir . '/gradelib.php'); $lineitem = \grade_item::fetch(array('id' => $itemid)); if ($lineitem) { $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid); if (!$gbs) { // We will need to check if the activity related belongs to our tool proxy. $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); if (($ltiactivity) && (isset($ltiactivity->typeid))) { if ($ltiactivity->typeid != 0) { $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); } else { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); } } if (is_null($typeid)) { if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) { return false; } } else { if (!(($tool) && ($tool->id == $typeid))) { return false; } } } else { return false; } } } return $lineitem; } /** * Adds a decoupled (standalone) line item. * Decoupled line items are not directly attached to * an lti instance activity. They are recorded in * the gradebook as manual activities and the * gradebookservices is used to associate that manual column * with the tool in addition to storing the LTI related * metadata (resource id, tag). * * @param string $courseid ID of course * @param string $label label of lineitem * @param float $maximumscore maximum score of lineitem * @param string $baseurl * @param int|null $ltilinkid id of lti instance this line item is associated with * @param string|null $resourceid resource id of lineitem * @param string|null $tag tag of lineitem * @param int $typeid lti type to which this line item is associated with * @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to * * @return int id of the created gradeitem */ public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore, string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid, ?int $toolproxyid = null): int { global $DB; $params = array(); $params['itemname'] = $label; $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $maximumscore; $params['grademin'] = 0; $item = new \grade_item(array('id' => 0, 'courseid' => $courseid)); \grade_item::set_properties($item, $params); $item->itemtype = 'manual'; $item->grademax = $maximumscore; $id = $item->insert('mod/ltiservice_gradebookservices'); $DB->insert_record('ltiservice_gradebookservices', (object)array( 'gradeitemid' => $id, 'courseid' => $courseid, 'toolproxyid' => $toolproxyid, 'typeid' => $typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltilinkid, 'resourceid' => $resourceid, 'tag' => $tag )); return $id; } /** * Set a grade item. * * @param object $gradeitem Grade Item record * @param object $score Result object * @param int $userid User ID * * @throws \Exception * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see gradebookservices::save_grade_item($gradeitem, $score, $userid) */ public static function save_score($gradeitem, $score, $userid) { $service = new gradebookservices(); $service->save_grade_item($gradeitem, $score, $userid); } /** * Saves a score received from the LTI tool. * * @param object $gradeitem Grade Item record * @param object $score Result object * @param int $userid User ID * * @throws \Exception */ public function save_grade_item($gradeitem, $score, $userid) { global $DB, $CFG; $source = 'mod' . $this->get_component_id(); if ($DB->get_record('user', array('id' => $userid)) === false) { throw new \Exception(null, 400); } require_once($CFG->libdir . '/gradelib.php'); $finalgrade = null; $timemodified = null; if (isset($score->scoreGiven)) { $finalgrade = grade_floatval($score->scoreGiven); $max = 1; if (isset($score->scoreMaximum)) { $max = $score->scoreMaximum; } if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) { // Rescale to match the grade item maximum. $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max); } if (isset($score->timestamp)) { $timemodified = strtotime($score->timestamp); } else { $timemodified = time(); } } $feedbackformat = FORMAT_MOODLE; $feedback = null; if (!empty($score->comment)) { $feedback = $score->comment; $feedbackformat = FORMAT_PLAIN; } if ($gradeitem->is_manual_item()) { $result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified); } else { if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) { $grade = new \grade_grade(); $grade->userid = $userid; $grade->itemid = $gradeitem->id; } $grade->rawgrademax = $score->scoreMaximum; $grade->timemodified = $timemodified; $grade->feedbackformat = $feedbackformat; $grade->feedback = $feedback; $grade->rawgrade = $finalgrade; $grade->dategraded = $timemodified; $status = grade_update($source, $gradeitem->courseid, $gradeitem->itemtype, $gradeitem->itemmodule, $gradeitem->iteminstance, $gradeitem->itemnumber, $grade); $result = ($status == GRADE_UPDATE_OK); } if (!$result) { debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid); throw new \Exception(null, 500); } } /** * Get the json object representation of the grade item * * @param object $item Grade Item record * @param string $endpoint Endpoint for lineitems container request * @param string $typeid * * @return object */ public static function item_for_json($item, $endpoint, $typeid) { $lineitem = new \stdClass(); if (is_null($typeid)) { $typeidstring = ""; } else { $typeidstring = "?type_id={$typeid}"; } $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring; $lineitem->label = $item->itemname; $lineitem->scoreMaximum = floatval($item->grademax); $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id); if ($gbs) { $lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : ''; $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : ''; if (isset($gbs->ltilinkid)) { $lineitem->resourceLinkId = strval($gbs->ltilinkid); $lineitem->ltiLinkId = strval($gbs->ltilinkid); } if (!empty($gbs->subreviewurl)) { $submissionreview = new \stdClass(); if ($gbs->subreviewurl != 'DEFAULT') { $submissionreview->url = $gbs->subreviewurl; } if (!empty($gbs->subreviewparams)) { $submissionreview->custom = lti_split_parameters($gbs->subreviewparams); } $lineitem->submissionReview = $submissionreview; } } else { $lineitem->tag = ''; if (isset($item->iteminstance)) { $lineitem->resourceLinkId = strval($item->iteminstance); $lineitem->ltiLinkId = strval($item->iteminstance); } } return $lineitem; } /** * Get the object matching the JSON representation of the result. * * @param object $grade Grade record * @param string $endpoint Endpoint for lineitem * @param int $typeid The id of the type to include in the result url. * * @return object */ public static function result_for_json($grade, $endpoint, $typeid) { if (is_null($typeid)) { $id = "{$endpoint}/results?user_id={$grade->userid}"; } else { $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}"; } $result = new \stdClass(); $result->id = $id; $result->userId = $grade->userid; if (!empty($grade->finalgrade)) { $result->resultScore = floatval($grade->finalgrade); $result->resultMaximum = floatval($grade->rawgrademax); if (!empty($grade->feedback)) { $result->comment = $grade->feedback; } if (is_null($typeid)) { $result->scoreOf = $endpoint; } else { $result->scoreOf = "{$endpoint}?type_id={$typeid}"; } $result->timestamp = date('c', $grade->timemodified); } return $result; } /** * Check if an LTI id is valid. * * @param string $linkid The lti id * @param string $course The course * @param string $toolproxy The tool proxy id * * @return boolean */ public static function check_lti_id($linkid, $course, $toolproxy) { global $DB; // Check if lti type is zero or not (comes from a backup). $sqlparams1 = array(); $sqlparams1['linkid'] = $linkid; $sqlparams1['course'] = $course; $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); if ($ltiactivity->typeid == 0) { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); } return (($tool) && ($toolproxy == $tool->toolproxyid)); } else { $sqlparams2 = array(); $sqlparams2['linkid'] = $linkid; $sqlparams2['course'] = $course; $sqlparams2['toolproxy'] = $toolproxy; $sql = 'SELECT lti.* FROM {lti} lti INNER JOIN {lti_types} typ ON lti.typeid = typ.id WHERE lti.id = ? AND lti.course = ? AND typ.toolproxyid = ?'; return $DB->record_exists_sql($sql, $sqlparams2); } } /** * Check if an LTI id is valid when we are in a LTI 1.x case * * @param string $linkid The lti id * @param string $course The course * @param string $typeid The lti type id * * @return boolean */ public static function check_lti_1x_id($linkid, $course, $typeid) { global $DB; // Check if lti type is zero or not (comes from a backup). $sqlparams1 = array(); $sqlparams1['linkid'] = $linkid; $sqlparams1['course'] = $course; $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); if ($ltiactivity) { if ($ltiactivity->typeid == 0) { $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); if (!$tool) { $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); } return (($tool) && ($typeid == $tool->id)); } else { $sqlparams2 = array(); $sqlparams2['linkid'] = $linkid; $sqlparams2['course'] = $course; $sqlparams2['typeid'] = $typeid; $sql = 'SELECT lti.* FROM {lti} lti INNER JOIN {lti_types} typ ON lti.typeid = typ.id WHERE lti.id = ? AND lti.course = ? AND typ.id = ?'; return $DB->record_exists_sql($sql, $sqlparams2); } } else { return false; } } /** * Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance. * * @param object $ltiinstance The lti instance to which the grade item is coupled to * @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null. * @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null. * @param moodle_url|null $subreviewurl The submission review target link URL * @param string|null $subreviewparams The submission review custom parameters. * */ public static function update_coupled_gradebookservices(object $ltiinstance, ?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams): void { global $DB; if ($ltiinstance && $ltiinstance->typeid) { $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id)); if ($gradeitem) { $resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid; $subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null; $tag = (isset($tag) && empty(trim($tag))) ? null : $tag; $gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); if ($gbs) { $gbs->resourceid = $resourceid; $gbs->tag = $tag; $gbs->subreviewurl = $subreviewurlstr; $gbs->subreviewparams = $subreviewparams; $DB->update_record('ltiservice_gradebookservices', $gbs); } else { $baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl; $DB->insert_record('ltiservice_gradebookservices', (object)array( 'gradeitemid' => $gradeitem->id, 'courseid' => $gradeitem->courseid, 'typeid' => $ltiinstance->typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltiinstance->id, 'resourceid' => $resourceid, 'tag' => $tag, 'subreviewurl' => $subreviewurlstr, 'subreviewparams' => $subreviewparams )); } } } } /** * Called when a new LTI Instance is added. * * @param object $lti LTI Instance. */ public function instance_added(object $lti): void { self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, $lti->lineitemsubreviewparams ?? null); } /** * Called when a new LTI Instance is updated. * * @param object $lti LTI Instance. */ public function instance_updated(object $lti): void { self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null, isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null, $lti->lineitemsubreviewparams ?? null); } /** * Set the form data when displaying the LTI Instance form. * * @param object $defaultvalues Default form values. */ public function set_instance_form_values(object $defaultvalues): void { $defaultvalues->lineitemresourceid = ''; $defaultvalues->lineitemtag = ''; $defaultvalues->subreviewurl = ''; $defaultvalues->subreviewparams = ''; if (is_object($defaultvalues) && $defaultvalues->instance) { $gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance); if ($gbs) { $defaultvalues->lineitemresourceid = $gbs->resourceid; $defaultvalues->lineitemtag = $gbs->tag; $defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl; $defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams; } } } /** * Deletes orphaned rows from the 'ltiservice_gradebookservices' table. * * Sometimes, if a gradebook entry is deleted and it was a lineitem * the row in the table ltiservice_gradebookservices can become an orphan * This method will clean these orphans. It will happens based on a task * because it is not urgent and we don't want to slow the service */ public static function delete_orphans_ltiservice_gradebookservices_rows() { global $DB; $sql = "DELETE FROM {ltiservice_gradebookservices} WHERE gradeitemid NOT IN (SELECT id FROM {grade_items} gi)"; $DB->execute($sql); } /** * Check if a user can be graded in a course * * @param int $courseid The course * @param int $userid The user * @return bool */ public static function is_user_gradable_in_course($courseid, $userid) { global $CFG; $gradableuser = false; $coursecontext = \context_course::instance($courseid); if (is_enrolled($coursecontext, $userid, '', false)) { $roles = get_user_roles($coursecontext, $userid); $gradebookroles = explode(',', $CFG->gradebookroles); foreach ($roles as $role) { foreach ($gradebookroles as $gradebookrole) { if ($role->roleid === $gradebookrole) { $gradableuser = true; } } } } return $gradableuser; } /** * Find the right element in the ltiservice_gradebookservice table for an lti instance * * @param string $instanceid The LTI module instance id * @return object gradebookservice for this line item */ public static function find_ltiservice_gradebookservice_for_lti($instanceid) { global $DB; if ($instanceid) { $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid)); if ($gradeitem) { return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id); } } } /** * Find the right element in the ltiservice_gradebookservice table for a lineitem * * @param string $lineitemid The lineitem (gradeitem) id * @return object gradebookservice if it exists */ public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) { global $DB; if ($lineitemid) { return $DB->get_record('ltiservice_gradebookservices', array('gradeitemid' => $lineitemid)); } } /** * Validates specific ISO 8601 format of the timestamps. * * @param string $date The timestamp to check. * @return boolean true or false if the date matches the format. * */ public static function validate_iso8601_date($date) { if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' . '(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))' . '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' . '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) { return true; } else { return false; } } } service/gradebookservices/classes/local/resources/results.php000064400000027006152265520610020671 0ustar00. /** * This file contains a class definition for the LISResults container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LISResult container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class results extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'Result.collection'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem/results'; $this->variables[] = 'Results.url'; $this->formats[] = 'application/vnd.ims.lis.v2.resultcontainer+json'; $this->methods[] = 'GET'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; $isget = $response->get_request_method() === self::HTTP_GET; // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_RESULT_READ; try { if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); $ltilinkid = null; if (isset($item->iteminstance)) { $ltilinkid = $item->iteminstance; } else if ($gbs && isset($gbs->ltilinkid)) { $ltilinkid = $gbs->ltilinkid; } if ($ltilinkid != null) { if (is_null($typeid)) { if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } else { if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } } require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case 'GET': $useridfilter = optional_param('user_id', 0, PARAM_INT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); $typeid = optional_param('type_id', null, PARAM_TEXT); $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum, $useridfilter, $typeid, $response); $response->set_content_type($this->formats[0]); $response->set_body($json); break; default: // Should not be possible. $response->set_code(405); $response->set_reason("Invalid request method specified."); return; } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a GET request. * * @param int $itemid Grade item instance ID * @param int $limitfrom Offset for the first result to include in this paged set * @param int $limitnum Maximum number of results to include in the response, ignored if zero * @param int $useridfilter The user id to filter the results. * @param int $typeid Lti tool typeid (or null) * @param \mod_lti\local\ltiservice\response $response The response element needed to add a header. * * @return string */ private function get_json_for_get_request($itemid, $limitfrom, $limitnum, $useridfilter, $typeid, $response) { if ($useridfilter > 0) { $grades = \grade_grade::fetch_all(array('itemid' => $itemid, 'userid' => $useridfilter)); } else { $grades = \grade_grade::fetch_all(array('itemid' => $itemid)); } $firstpage = null; $nextpage = null; $prevpage = null; $lastpage = null; if ($grades && isset($limitnum) && $limitnum > 0) { // Since we only display grades that have been modified, we need to filter first in order to support // paging. $resultgrades = array_filter($grades, function ($grade) { return !empty($grade->timemodified); }); // We save the total count to calculate the last page. $totalcount = count($resultgrades); // We slice to the requested item offset to insure proper item is always first, and we always return // first pageset of any remaining items. $grades = array_slice($resultgrades, $limitfrom); if (count($grades) > 0) { $pagedgrades = array_chunk($grades, $limitnum); $pageset = 0; $grades = $pagedgrades[$pageset]; } if ($limitfrom >= $totalcount || $limitfrom < 0) { $outofrange = true; } else { $outofrange = false; } $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; $limitcurrent = $limitfrom; $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; $limitfrom += $limitnum; $baseurl = new \moodle_url($this->get_endpoint()); if (is_null($typeid)) { $baseurl->param('limit', $limitnum); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } else { $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } } $jsonresults = []; $lineitem = new lineitem($this->get_service()); $endpoint = $lineitem->get_endpoint(); if ($grades) { foreach ($grades as $grade) { if (!empty($grade->timemodified)) { array_push($jsonresults, gradebookservices::result_for_json($grade, $endpoint, $typeid)); } } } if (isset($canonicalpage) && ($canonicalpage)) { $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; if (!is_null($prevpage)) { $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; } $links .= ', <' . $canonicalpage->out() . '>; rel=“canonical”'; if (!is_null($nextpage)) { $links .= ', <' . $nextpage->out() . '>; rel=“next”'; } $links .= ', <' . $lastpage->out() . '>; rel=“last”'; $response->add_additional_header($links); } return json_encode($jsonresults); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$Results.url') !== false) { require_once($CFG->libdir . '/gradelib.php'); $resolved = ''; $this->params['context_id'] = $COURSE->id; $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); } } $value = str_replace('$Results.url', $resolved, $value); } return $value; } } service/gradebookservices/classes/local/resources/lineitems.php000064400000030553152265520610021162 0ustar00. /** * This file contains a class definition for the LineItem container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LineItem container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lineitems extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LineItem.collection'; $this->template = '/{context_id}/lineitems'; $this->variables[] = 'LineItems.url'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitemcontainer+json'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; $this->methods[] = self::HTTP_GET; $this->methods[] = self::HTTP_POST; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $isget = $response->get_request_method() === self::HTTP_GET; if ($isget) { $contenttype = $response->get_accept(); } else { $contenttype = $response->get_content_type(); } $container = empty($contenttype) || ($contenttype === $this->formats[0]); // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM); if ($response->get_request_method() === self::HTTP_GET) { $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; } try { if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { throw new \Exception('No context or unsupported content type', 400); } if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if ($response->get_request_method() !== self::HTTP_POST) { $resourceid = optional_param('resource_id', null, PARAM_TEXT); $ltilinkid = optional_param('resource_link_id', null, PARAM_TEXT); if (is_null($ltilinkid)) { $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT); } $tag = optional_param('tag', null, PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); $itemsandcount = $this->get_service()->get_lineitems($contextid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid); $items = $itemsandcount[1]; $totalcount = $itemsandcount[0]; $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response); $response->set_content_type($this->formats[0]); } else { $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid); $response->set_code(201); $response->set_content_type($this->formats[1]); } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a GET request. * * @param array $items Array of lineitems * @param string $resourceid Resource identifier used for filtering, may be null * @param string $ltilinkid Resource Link identifier used for filtering, may be null * @param string $tag Tag identifier used for filtering, may be null * @param int $limitfrom Offset of the first line item to return * @param int $limitnum Maximum number of line items to return, ignored if zero or less * @param int $totalcount Number of total lineitems before filtering for paging * @param int $typeid Maximum number of line items to return, ignored if zero or less * @param \mod_lti\local\ltiservice\response $response * @return string */ private function get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) { $firstpage = null; $nextpage = null; $prevpage = null; $lastpage = null; if (isset($limitnum) && $limitnum > 0) { if ($limitfrom >= $totalcount || $limitfrom < 0) { $outofrange = true; } else { $outofrange = false; } $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; $limitcurrent = $limitfrom; $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; $limitfrom += $limitnum; $baseurl = new \moodle_url($this->get_endpoint()); if (isset($resourceid)) { $baseurl->param('resource_id', $resourceid); } if (isset($ltilinkid)) { $baseurl->param('resource_link_id', $ltilinkid); } if (isset($tag)) { $baseurl->param('tag', $tag); } if (is_null($typeid)) { $baseurl->param('limit', $limitnum); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' > $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } else { $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); } $firstpage = new \moodle_url($baseurl, ['from' => 0]); $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); if (($limitcurrent > 0) && (!$outofrange)) { $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); } } } $jsonitems = []; $endpoint = parent::get_endpoint(); foreach ($items as $item) { array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid)); } if (isset($canonicalpage) && ($canonicalpage)) { $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; if (!is_null($prevpage)) { $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; } $links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”'; if (!is_null($nextpage)) { $links .= ', <' . $nextpage->out() . '>; rel=“next”'; } $links .= ', <' . $lastpage->out() . '>; rel=“last”'; $response->add_additional_header($links); } return json_encode($jsonitems); } /** * Generate the JSON for a POST request. * * @param string $body POST body * @param string $contextid Course ID * @param string $typeid * * @return string * @throws \Exception */ private function get_json_for_post_request($body, $contextid, $typeid) { global $CFG, $DB; $json = json_decode($body); if (empty($json) || !isset($json->scoreMaximum) || !isset($json->label)) { throw new \Exception('No label or Score Maximum', 400); } if (is_numeric($json->scoreMaximum)) { $max = $json->scoreMaximum; } else { throw new \Exception(null, 400); } require_once($CFG->libdir.'/gradelib.php'); $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; $ltilinkid = (isset($json->resourceLinkId)) ? $json->resourceLinkId : null; if ($ltilinkid == null) { $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null; } if ($ltilinkid != null) { if (is_null($typeid)) { if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) { throw new \Exception(null, 403); } } else { if (!gradebookservices::check_lti_1x_id($ltilinkid, $contextid, $typeid)) { throw new \Exception(null, 403); } } } $tag = (isset($json->tag)) ? $json->tag : ''; if (is_null($typeid)) { $toolproxyid = $this->get_service()->get_tool_proxy()->id; $baseurl = null; } else { $toolproxyid = null; $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; } $gradebookservices = new gradebookservices(); $id = $gradebookservices->add_standalone_lineitem($contextid, $json->label, $max, $baseurl, $ltilinkid, $resourceid, $tag, $typeid, $toolproxyid); if (is_null($typeid)) { $json->id = parent::get_endpoint() . "/{$id}/lineitem"; } else { $json->id = parent::get_endpoint() . "/{$id}/lineitem?type_id={$typeid}"; } return json_encode($json, JSON_UNESCAPED_SLASHES); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE; if (strpos($value, '$LineItems.url') !== false) { $this->params['context_id'] = $COURSE->id; $query = ''; if (($tool = $this->get_service()->get_type())) { $query = "?type_id={$tool->id}"; } $value = str_replace('$LineItems.url', parent::get_endpoint() . $query, $value); } return $value; } } service/gradebookservices/classes/local/resources/lineitem.php000064400000033201152265520610020770 0ustar00. /** * This file contains a class definition for the LineItem resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LineItem. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lineitem extends resource_base { /** * Class constructor. * * @param gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LineItem.item'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem'; $this->variables[] = 'LineItem.url'; $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; $this->methods[] = self::HTTP_GET; $this->methods[] = self::HTTP_PUT; $this->methods[] = self::HTTP_DELETE; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; $isget = $response->get_request_method() === self::HTTP_GET; // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_INT); $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM); if ($response->get_request_method() === self::HTTP_GET) { $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ; } try { if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case self::HTTP_GET: $this->get_request($response, $item, $typeid); break; case self::HTTP_PUT: $json = $this->process_put_request($response->get_request_data(), $item, $typeid); $response->set_body($json); $response->set_code(200); break; case self::HTTP_DELETE: $this->process_delete_request($item); $response->set_code(204); break; } } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Process a GET request. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. * @param object $item Grade item instance. * @param string $typeid Tool Type Id */ private function get_request($response, $item, $typeid) { $response->set_content_type($this->formats[0]); $lineitem = gradebookservices::item_for_json($item, substr(parent::get_endpoint(), 0, strrpos(parent::get_endpoint(), "/", -10)), $typeid); $response->set_body(json_encode($lineitem)); } /** * Process a PUT request. * * @param string $body PUT body * @param \ltiservice_gradebookservices\local\resources\lineitem $olditem Grade item instance * @param string $typeid Tool Type Id * * @return string * @throws \Exception */ private function process_put_request($body, $olditem, $typeid) { global $DB; $json = json_decode($body); if (empty($json) || !isset($json->scoreMaximum) || !isset($json->label)) { throw new \Exception(null, 400); } $item = \grade_item::fetch(array('id' => $olditem->id, 'courseid' => $olditem->courseid)); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($olditem->id); $updategradeitem = false; $rescalegrades = false; $oldgrademax = grade_floatval($item->grademax); $upgradegradebookservices = false; if ($item->itemname !== $json->label) { $updategradeitem = true; } $item->itemname = $json->label; if (!is_numeric($json->scoreMaximum)) { throw new \Exception(null, 400); } else { if (grade_floats_different($oldgrademax, grade_floatval($json->scoreMaximum))) { $updategradeitem = true; $rescalegrades = true; } $item->grademax = grade_floatval($json->scoreMaximum); } if ($gbs) { $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; $tag = (isset($json->tag)) ? $json->tag : ''; if ($gbs->tag !== $tag || $gbs->resourceid !== $resourceid) { $upgradegradebookservices = true; } $gbs->tag = $tag; $gbs->resourceid = $resourceid; $incomingurl = null; $incomingparams = null; if (isset($json->submissionReview)) { $incomingurl = $json->submissionReview->url ?? 'DEFAULT'; if (isset($json->submissionReview->custom)) { $incomingparams = params_to_string($json->submissionReview->custom); } } if ($gbs->subreviewurl ?? null !== $incomingurl || $gbs->subreviewparams ?? null !== $incomingparams) { $upgradegradebookservices = true; $gbs->subreviewurl = $incomingurl; $gbs->subreviewparams = $incomingparams; } } $ltilinkid = null; if (isset($json->resourceLinkId)) { if (is_numeric($json->resourceLinkId)) { $ltilinkid = $json->resourceLinkId; if ($gbs) { if (intval($gbs->ltilinkid) !== intval($json->resourceLinkId)) { $gbs->ltilinkid = $json->resourceLinkId; $upgradegradebookservices = true; } } else { if (intval($item->iteminstance) !== intval($json->resourceLinkId)) { $item->iteminstance = intval($json->resourceLinkId); $updategradeitem = true; } } } else { throw new \Exception(null, 400); } } else if (isset($json->ltiLinkId)) { if (is_numeric($json->ltiLinkId)) { $ltilinkid = $json->ltiLinkId; if ($gbs) { if (intval($gbs->ltilinkid) !== intval($json->ltiLinkId)) { $gbs->ltilinkid = $json->ltiLinkId; $upgradegradebookservices = true; } } else { if (intval($item->iteminstance) !== intval($json->ltiLinkId)) { $item->iteminstance = intval($json->ltiLinkId); $updategradeitem = true; } } } else { throw new \Exception(null, 400); } } if ($ltilinkid != null) { if (is_null($typeid)) { if (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id)) { throw new \Exception(null, 403); } } else { if (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid)) { throw new \Exception(null, 403); } } } if ($updategradeitem) { if (!$item->update('mod/ltiservice_gradebookservices')) { throw new \Exception(null, 500); } if ($rescalegrades) { $item->rescale_grades_keep_percentage(0, $oldgrademax, 0, $item->grademax); } } $lineitem = new lineitem($this->get_service()); $endpoint = $lineitem->get_endpoint(); if ($upgradegradebookservices) { if (is_null($typeid)) { $toolproxyid = $this->get_service()->get_tool_proxy()->id; $baseurl = null; } else { $toolproxyid = null; $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; } $DB->update_record('ltiservice_gradebookservices', (object)array( 'id' => $gbs->id, 'gradeitemid' => $gbs->gradeitemid, 'courseid' => $gbs->courseid, 'toolproxyid' => $toolproxyid, 'typeid' => $typeid, 'baseurl' => $baseurl, 'ltilinkid' => $ltilinkid, 'resourceid' => $resourceid, 'tag' => $gbs->tag, 'subreviewurl' => $gbs->subreviewurl, 'subreviewparams' => $gbs->subreviewparams )); } if (is_null($typeid)) { $id = "{$endpoint}"; $json->id = $id; } else { $id = "{$endpoint}?type_id={$typeid}"; $json->id = $id; } return json_encode($json, JSON_UNESCAPED_SLASHES); } /** * Process a DELETE request. * * @param \ltiservice_gradebookservices\local\resources\lineitem $item Grade item instance * @throws \Exception */ private function process_delete_request($item) { global $DB; $gradeitem = \grade_item::fetch(array('id' => $item->id)); if (($gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($item->id)) == false) { throw new \Exception(null, 403); } if (!$gradeitem->delete('mod/ltiservice_gradebookservices')) { throw new \Exception(null, 500); } else { $sqlparams = array(); $sqlparams['id'] = $gbs->id; if (!$DB->delete_records('ltiservice_gradebookservices', $sqlparams)) { throw new \Exception(null, 500); } } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$LineItem.url') !== false) { $resolved = ''; require_once($CFG->libdir . '/gradelib.php'); $this->params['context_id'] = $COURSE->id; if ($tool = $this->get_service()->get_type()) { $this->params['tool_code'] = $tool->id; } $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (empty($id)) { $hint = optional_param('lti_message_hint', "", PARAM_TEXT); if ($hint) { $hintdec = json_decode($hint); if (isset($hintdec->cmid)) { $id = $hintdec->cmid; } } } if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); $resolved .= "?type_id={$tool->id}"; } } $value = str_replace('$LineItem.url', $resolved, $value); } return $value; } } service/gradebookservices/classes/local/resources/scores.php000064400000023407152265520610020467 0ustar00. /** * This file contains a class definition for the LISResult container resource * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\local\resources; use ltiservice_gradebookservices\local\service\gradebookservices; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing LISResult container. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class scores extends resource_base { /** * Class constructor. * * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'Score.collection'; $this->template = '/{context_id}/lineitems/{item_id}/lineitem/scores'; $this->variables[] = 'Scores.url'; $this->formats[] = 'application/vnd.ims.lis.v1.scorecontainer+json'; $this->formats[] = 'application/vnd.ims.lis.v1.score+json'; $this->methods[] = 'POST'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG, $DB; $params = $this->parse_template(); $contextid = $params['context_id']; $itemid = $params['item_id']; // GET is disabled by the moment, but we have the code ready // for a future implementation. $isget = $response->get_request_method() === 'GET'; if ($isget) { $contenttype = $response->get_accept(); } else { $contenttype = $response->get_content_type(); } $container = empty($contenttype) || ($contenttype === $this->formats[0]); // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. $typeid = optional_param('type_id', null, PARAM_ALPHANUM); $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_SCORE; try { if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) { throw new \Exception(null, 401); } $typeid = $this->get_service()->get_type()->id; if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { throw new \Exception('No context or unsupported content type', 400); } if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) { throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404); } if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) { throw new \Exception('Not allowed in context', 403); } if (!$DB->record_exists('grade_items', array('id' => $itemid))) { throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404); } $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); if ($item === false) { throw new \Exception('Line item does not exist', 404); } $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); $ltilinkid = null; if (isset($item->iteminstance)) { $ltilinkid = $item->iteminstance; } else if ($gbs && isset($gbs->ltilinkid)) { $ltilinkid = $gbs->ltilinkid; } if ($ltilinkid != null) { if (is_null($typeid)) { if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, $this->get_service()->get_tool_proxy()->id))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } else { if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, $typeid))) { $response->set_code(403); $response->set_reason("Invalid LTI id supplied."); return; } } } $json = '[]'; require_once($CFG->libdir.'/gradelib.php'); switch ($response->get_request_method()) { case 'GET': $response->set_code(405); $response->set_reason("GET requests are not allowed."); break; case 'POST': try { $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid, $typeid); $response->set_content_type($this->formats[1]); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } break; default: // Should not be possible. $response->set_code(405); $response->set_reason("Invalid request method specified."); return; } $response->set_body($json); } catch (\Exception $e) { $response->set_code($e->getCode()); $response->set_reason($e->getMessage()); } } /** * Generate the JSON for a POST request. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. * @param string $body POST body * @param object $item Grade item instance * @param string $contextid * @param string $typeid * * @throws \Exception */ private function get_json_for_post_request($response, $body, $item, $contextid, $typeid) { $score = json_decode($body); if (empty($score) || !isset($score->userId) || !isset($score->timestamp) || !isset($score->gradingProgress) || !isset($score->activityProgress) || !isset($score->timestamp) || isset($score->timestamp) && !gradebookservices::validate_iso8601_date($score->timestamp) || (isset($score->scoreGiven) && !is_numeric($score->scoreGiven)) || (isset($score->scoreGiven) && !isset($score->scoreMaximum)) || (isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) || (!gradebookservices::is_user_gradable_in_course($contextid, $score->userId)) ) { throw new \Exception('Incorrect score received' . $body, 400); } $score->timemodified = intval($score->timestamp); if (!isset($score->scoreMaximum)) { $score->scoreMaximum = 1; } $response->set_code(200); $grade = \grade_grade::fetch(array('itemid' => $item->id, 'userid' => $score->userId)); if ($grade && !empty($grade->timemodified)) { if ($grade->timemodified >= strtotime($score->timestamp)) { $exmsg = "Refusing score with an earlier timestamp for item " . $item->id . " and user " . $score->userId; throw new \Exception($exmsg, 409); } } if (isset($score->scoreGiven)) { if ($score->gradingProgress != 'FullyGraded') { $score->scoreGiven = null; } } $this->get_service()->save_grade_item($item, $score, $score->userId); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE, $CFG; if (strpos($value, '$Scores.url') !== false) { require_once($CFG->libdir . '/gradelib.php'); $resolved = ''; $this->params['context_id'] = $COURSE->id; $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $id = $cm->instance; $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); if ($item && $item->items) { $this->params['item_id'] = $item->items[0]->id; $resolved = parent::get_endpoint(); } } $value = str_replace('$Scores.url', $resolved, $value); } return $value; } } service/gradebookservices/classes/task/cleanup_task.php000064400000003556152265520610017503 0ustar00. /** * A scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\task; use core\task\scheduled_task; use ltiservice_gradebookservices\local\service\gradebookservices; defined('MOODLE_INTERNAL') || die(); /** * Class containing the scheduled task for gradebookservices. * * @package ltiservice_gradebookservices * @copyright 2017 Cengage Learning http://www.cengage.com * @author Dirk Singels, Diego del Blanco, Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cleanup_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('taskcleanup', 'ltiservice_gradebookservices'); } /** * Run forum cron. */ public function execute() { gradebookservices::delete_orphans_ltiservice_gradebookservices_rows(); } } service/gradebookservices/upgrade.txt000064400000000557152265520610014110 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the lti code. === 4.1 === * The update_coupled_gradebookservices function now accept 2 additional optional parameters to update the newly added properties related to submission review support (url and custom params). service/gradebookservices/tests/privacy/provider_test.php000064400000003453152265520610020137 0ustar00. /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @package ltiservice_gradebookservices * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id); $this->assertEmpty($contextlist); } } service/gradebookservices/tests/lineitem_test.php000064400000036300152265520610016433 0ustar00. namespace ltiservice_gradebookservices; use ltiservice_gradebookservices\local\resources\lineitem; use ltiservice_gradebookservices\local\resources\results; use ltiservice_gradebookservices\local\resources\scores; use ltiservice_gradebookservices\local\service\gradebookservices; /** * Unit tests for lti lineitem. * * @package ltiservice_gradebookservices * @category test * @copyright 2022 Cengage Group * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\service\gradebookservices\local\resources\lineitem */ final class lineitem_test extends \advanced_testcase { /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_nosubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($tag.'modified', $lineitem->tag); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); } /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_withsubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $subreviewparams = 'a=2'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertTrue(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $lineitem->submissionReview->url = $subreviewurl.'modified'; $lineitem->submissionReview->custom = ['a' => '3']; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($subreviewurl.'modified', $lineitem->submissionReview->url); $custom = $lineitem->submissionReview->custom; $this->assertEquals('a=3', join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom)))); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); $this->assertEquals($subreviewurl.'modified', $responseitem->submissionReview->url); } /** * @covers ::execute * * Test updating the line item with submission review. */ public function test_execute_put_addsubreview(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $this->setAdminUser(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); // The 1st item in the array is the items count. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertFalse(isset($lineitem->submissionReview)); $lineitemresource = new lineitem($gbservice); $this->set_server_for_put($course, $typeid, $lineitem); $response = new \mod_lti\local\ltiservice\response(); $lineitem->resourceId = $resourceid.'modified'; $lineitem->tag = $tag.'modified'; $lineitem->submissionReview = ['url' => $subreviewurl]; $response->set_request_data(json_encode($lineitem)); $lineitemresource->execute($response); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals($resourceid.'modified', $lineitem->resourceId); $this->assertEquals($subreviewurl, $lineitem->submissionReview->url); $this->assertFalse(isset($lineitem->submissionReview->custom)); $responseitem = json_decode($response->get_body()); $this->assertEquals($resourceid.'modified', $responseitem->resourceId); $this->assertEquals($subreviewurl, $responseitem->submissionReview->url); $this->assertFalse(isset($responseitem->submissionReview->custom)); } /** * Test running a series of score updates, highlighting problems with the score posting logic. * * @covers ::execute * * @return void */ public function test_sequential_score_posts(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->resetAfterTest(); $resourceid = 'test-resource-id'; $tag = 'tag'; $course = $this->getDataGenerator()->create_course(); $typeid = $this->create_type(); $user = $this->getDataGenerator()->create_and_enrol($course); // Create mod instance with line item - nothing pushed via services yet. $gbservice = new gradebookservices(); $gbservice->set_type(lti_get_type($typeid)); $modinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); // The 1st item in the array is the items count. // Post a score so that there's at least one grade present at the time of lineitem update. $score = new scores($gbservice); $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata = [ 'scoreGiven' => "8.0", 'scoreMaximum' => "10.0", 'activityProgress' => "Completed", 'timestamp' => "2024-08-07T18:54:36.736+00:00", 'gradingProgress' => "FullyGraded", 'userId' => $user->id, ]; $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); // The grade in Moodle should reflect the score->timestamp, not the time of the score post. $grades = grade_get_grades($course->id, 'mod', 'lti', $modinstance->id, $user->id); $studentgrade = array_shift($grades->items[0]->grades); $this->assertEquals(strtotime($requestdata['timestamp']), $studentgrade->dategraded); // Read the results via the service. This should also return the correct dategraded time (timemodified in LTI terms). $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_GET; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/results?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $result = new results($gbservice); $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v2.resultcontainer+json'); $result->execute($response); $body = json_decode($response->get_body()); $result = array_shift($body); $this->assertEquals(strtotime($requestdata['timestamp']), strtotime($result->timestamp)); // Now, try to post a newer score using a timestamp that is greater than the one originally sent, but less than the time at // which the score was last posted. This should be valid since the timestamp is greater than the original posted score. $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata['scoreGiven'] = "14"; $requestdata['timestamp'] = "2024-08-08T18:54:36.736+00:00"; $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); $this->assertEquals(200, json_decode($response->get_code())); // Finally, post a score that's just been updated (i.e. score->timestamp = now). $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST; $_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; $requestdata['scoreGiven'] = "15"; $requestdata['timestamp'] = date('c', time()); $response = new \mod_lti\local\ltiservice\response(); $response->set_content_type('application/vnd.ims.lis.v1.score+json'); $response->set_request_data(json_encode($requestdata)); $score->execute($response); $this->assertEquals(200, json_decode($response->get_code())); } /** * Inserts a graded lti instance, which should create a grade_item and gradebookservices record. * * @param int $typeid Type ID of the LTI Tool. * @param object $course course where to add the lti instance. * @param string|null $resourceid resource id * @param string|null $tag tag * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params * * @return object lti instance created */ private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS, 'grade' => 10, 'lineitemresourceid' => $resourceid, 'lineitemtag' => $tag, 'lineitemsubreviewurl' => $subreviewurl, 'lineitemsubreviewparams' => $subreviewparams]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Creates a new LTI Tool Type. */ private function create_type() { $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->ltiservice_gradesynchronization = 2; return lti_add_type($type, $config); } /** * Sets the server info and get to be configured for a PUT operation, * including having a proper auth token attached. * * @param object $course course where to add the lti instance. * @param int $typeid * @param object $lineitem */ private function set_server_for_put(object $course, int $typeid, object $lineitem) { $_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_PUT; $_SERVER['PATH_INFO'] = "/$course->id/lineitems$lineitem->id"; $token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem']); $_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token; $_GET['type_id'] = (string)$typeid; } } service/gradebookservices/tests/task/cleanup_test.php000064400000010571152265520610017220 0ustar00. namespace ltiservice_gradebookservices\task; /** * Tests cleaning up the gradebook services task. * * @package ltiservice_gradebookservices * @category test * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cleanup_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the cleanup task. */ public function test_cleanup_task(): void { global $DB; // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a few LTI items. $lti = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $lti2 = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); $conditions = [ 'courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id ]; // Get the grade items. $gradeitem = $DB->get_record('grade_items', $conditions); $conditions['iteminstance'] = $lti2->id; $gradeitem2 = $DB->get_record('grade_items', $conditions); // Insert these into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); $data->gradeitemid = $gradeitem2->id; $DB->insert_record('ltiservice_gradebookservices', $data); $task = new cleanup_task(); $task->execute(); // Check they both still exist. $this->assertEquals(2, $DB->count_records('ltiservice_gradebookservices')); // Delete the first LTI activity. course_delete_module($lti->cmid); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check only the second grade item exists. $gradebookserviceitems = $DB->get_records('ltiservice_gradebookservices'); $this->assertCount(1, $gradebookserviceitems); $gradebookserviceitem = reset($gradebookserviceitems); $this->assertEquals($gradeitem2->id, $gradebookserviceitem->gradeitemid); } /** * Test the cleanup task with a manual grade item. */ public function test_cleanup_task_with_manual_item(): void { global $CFG, $DB; // This is required when running the unit test in isolation. require_once($CFG->libdir . '/gradelib.php'); // Create a manual grade item for a course. $course = $this->getDataGenerator()->create_course(); $params = [ 'courseid' => $course->id, 'itemtype' => 'manual' ]; $gradeitem = new \grade_item($params); $gradeitem->insert(); // Insert it into the 'ltiservice_gradebookservices' table. $data = new \stdClass(); $data->gradeitemid = $gradeitem->id; $data->courseid = $course->id; $DB->insert_record('ltiservice_gradebookservices', $data); // Run the task. $task = new cleanup_task(); $task->execute(); // Check it still exist. $this->assertEquals(1, $DB->count_records('ltiservice_gradebookservices')); // Delete the manual item. $gradeitem->delete(); // Run the task again. $task = new cleanup_task(); $task->execute(); // Check it has been removed. $this->assertEquals(0, $DB->count_records('ltiservice_gradebookservices')); } } service/gradebookservices/tests/gradebookservices_test.php000064400000040724152265520610020333 0ustar00. namespace ltiservice_gradebookservices; use ltiservice_gradebookservices\local\service\gradebookservices; /** * Unit tests for lti gradebookservices. * * @package ltiservice_gradebookservices * @category test * @copyright 2020 Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\service\gradebookservices\local\gradebookservices */ final class gradebookservices_test extends \advanced_testcase { /** * Load the necessary libs for the tests. */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); parent::setUpBeforeClass(); } /** * @covers ::instance_added * * Test saving a graded LTI with resource and tag info (as a result of * content item selection) creates a gradebookservices record * that can be retrieved using the gradebook service API. */ public function test_lti_add_coupled_lineitem(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $resourceid = 'test-resource-id'; $tag = 'tag'; $subreviewurl = 'https://subreview.example.com'; $subreviewparams = 'a=2'; $ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams); $this->assertNotNull($ltiinstance); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id); $this->assertNotNull($gbs); $this->assertEquals($resourceid, $gbs->resourceid); $this->assertEquals($tag, $gbs->tag); $this->assertEquals($subreviewurl, $gbs->subreviewurl); $this->assertEquals($subreviewparams, $gbs->subreviewparams); $this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $tag, $subreviewurl, $subreviewparams); } /** * @covers ::instance_added * * Test saving a graded LTI with resource and tag info (as a result of * content item selection) creates a gradebookservices record * that can be retrieved using the gradebook service API. */ public function test_lti_add_coupled_lineitem_default_subreview(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $resourceid = 'test-resource-id'; $tag = 'tag'; $ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, 'DEFAULT'); $this->assertNotNull($ltiinstance); $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id); $this->assertNotNull($gbs); $this->assertEquals('DEFAULT', $gbs->subreviewurl); $this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $tag, 'DEFAULT'); } /** * @covers ::add_standalone_lineitem * * Test saving a standalone LTI lineitem with resource and tag info * that can be retrieved using the gradebook service API. */ public function test_lti_add_standalone_lineitem(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $resourceid = "test-resource-standalone"; $tag = "test-tag-standalone"; $typeid = $this->create_type(); $this->create_standalone_lineitem($course->id, $typeid, $resourceid, $tag); $this->assert_lineitems($course, $typeid, "manualtest", null, $resourceid, $tag); } /** * @covers ::find_ltiservice_gradebookservice_for_lti * * Test line item URL is populated for coupled line item only * if there is not another line item bound to the lti instance, * since in that case there would be no rule to define which of * the line items should be actually passed. */ public function test_get_launch_parameters_coupled(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://subreview.url', 'sub=review'); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItem.url', $params['lineitem_url']); $this->assertEquals('$LineItem.url', $params['lineitem_url']); $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); // 2 line items for a single link, we cannot return a single line item url. $this->assertFalse(array_key_exists('$LineItem.url', $params)); } /** * @covers ::override_endpoint * * Test Submission Review URL and custom parameter is applied when the * launch is submission review. */ public function test_get_launch_parameters_coupled_subreview_override(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://example.com/subreview', 'action=review'); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti', "color=blue", $course->id, $ltiinstance); $this->assertEquals('https://example.com/subreview', $overrides[0]); $this->assertEquals("color=blue\naction=review", $overrides[1]); } /** * @covers ::override_endpoint * * Test Submission Review URL and custom parameter is applied when the * launch is submission review. */ public function test_get_launch_parameters_coupled_subreview_override_default(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'DEFAULT', ''); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti', "color=blue", $course->id, $ltiinstance); $this->assertEquals('https://example.com/lti', $overrides[0]); $this->assertEquals("color=blue", $overrides[1]); } /** * @covers ::get_launch_parameters * * Test line item URL is populated for not coupled line item only * if there is a single line item attached to that lti instance. */ public function test_get_launch_parameters_decoupled(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $typeid = $this->create_type(); $course = $this->getDataGenerator()->create_course(); $ltiinstance = $this->create_notgraded_lti($typeid, $course); $this->assertNotNull($ltiinstance); $gbservice = new gradebookservices(); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); $this->assertFalse(array_key_exists('$LineItem.url', $params)); $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id); $params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id); $this->assertEquals('$LineItems.url', $params['lineitems_url']); $this->assertEquals('$LineItem.url', $params['lineitem_url']); // 2 line items for a single link, we cannot return a single line item url. $this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag-2', $ltiinstance->id); $this->assertFalse(array_key_exists('$LineItem.url', $params)); } /** * @covers ::is_user_gradable_in_course * * Test if a user can be graded in a course. */ public function test_is_user_gradable_in_course(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $generator->enrol_user($user1->id, $course->id, 'student'); $generator->enrol_user($user2->id, $course->id, 'editingteacher'); $this->assertTrue(gradebookservices::is_user_gradable_in_course($course->id, $user1->id)); $this->assertFalse(gradebookservices::is_user_gradable_in_course($course->id, $user2->id)); } /** * Asserts a matching gradebookservices record exist with the matching tag and resourceid. * * @param object $course current course * @param int $typeid Type id of the tool * @param string $label Label of the line item * @param object|null $ltiinstance lti instance related to that line item * @param string|null $resourceid resourceid the line item should have * @param string|null $tag tag the line item should have * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params */ private function assert_lineitems(object $course, int $typeid, string $label, ?object $ltiinstance, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): void { $gbservice = new gradebookservices(); $gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid); // The 1st item in the array is the items count. $this->assertEquals(1, $gradeitems[0]); $lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid); $this->assertEquals(10, $lineitem->scoreMaximum); $this->assertEquals($resourceid, $lineitem->resourceId); $this->assertEquals($tag, $lineitem->tag); $this->assertEquals($label, $lineitem->label); $this->assertEquals(!empty($subreviewurl), isset($lineitem->submissionReview)); if ($subreviewurl) { if ($subreviewurl == 'DEFAULT') { $this->assertFalse(isset($lineitem->submissionReview->url)); } else { $this->assertEquals($subreviewurl, $lineitem->submissionReview->url); } if ($subreviewparams) { $custom = $lineitem->submissionReview->custom; $this->assertEquals($subreviewparams, join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom)))); } else { $this->assertFalse(isset($lineitem->submissionReview->custom)); } } $gradeitems = $gbservice->get_lineitems($course->id, $resourceid, null, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); if (isset($ltiinstance)) { $gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id, null, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id + 1, null, null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); } $gradeitems = $gbservice->get_lineitems($course->id, null, null, $tag, null, null, $typeid); $this->assertEquals(1, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, 'an unknown resource id', null, null, null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); $gradeitems = $gbservice->get_lineitems($course->id, null, null, 'an unknown tag', null, null, $typeid); $this->assertEquals(0, $gradeitems[0]); } /** * Inserts a graded lti instance, which should create a grade_item and gradebookservices record. * * @param int $typeid Type ID of the LTI Tool. * @param object $course course where to add the lti instance. * @param string|null $resourceid resource id * @param string|null $tag tag * @param string|null $subreviewurl submission review url * @param string|null $subreviewparams submission review custom params * * @return object lti instance created */ private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag, ?string $subreviewurl = null, ?string $subreviewparams = null): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS, 'grade' => 10, 'lineitemresourceid' => $resourceid, 'lineitemtag' => $tag, 'lineitemsubreviewurl' => $subreviewurl, 'lineitemsubreviewparams' => $subreviewparams]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Inserts an lti instance that is not graded. * * @param int $typeid Type Id of the LTI Tool. * @param object $course course where to add the lti instance. * * @return object lti instance created */ private function create_notgraded_lti(int $typeid, object $course): object { $lti = ['course' => $course->id, 'typeid' => $typeid, 'instructorchoiceacceptgrades' => LTI_SETTING_NEVER]; return $this->getDataGenerator()->create_module('lti', $lti, array()); } /** * Inserts a standalone lineitem (gradeitem, gradebookservices entries). * * @param int $courseid Id of the course where the standalone line item will be added. * @param int $typeid of the LTI Tool * @param string|null $resourceid resource id * @param string|null $tag tag * @param int|null $ltiinstanceid Id of the LTI instance the standalone line item will be related to. * */ private function create_standalone_lineitem(int $courseid, int $typeid, ?string $resourceid, ?string $tag, ?int $ltiinstanceid = null): void { $gbservice = new gradebookservices(); $gbservice->add_standalone_lineitem($courseid, "manualtest", 10, "https://test.phpunit", $ltiinstanceid, $resourceid, $tag, $typeid, null /*toolproxyid*/); } /** * Creates a new LTI Tool Type. */ private function create_type() { $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->ltiservice_gradesynchronization = 2; return lti_add_type($type, $config); } } service/toolsettings/lang/en/ltiservice_toolsettings.php000064400000002527152265520610017777 0ustar00. /** * Strings for component 'ltiservice_toolsettings', language 'en' * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['allow'] = 'Use this service'; $string['ltiservice_toolsettings'] = 'Tool Settings'; $string['ltiservice_toolsettings_help'] = 'Allow the tool to save and retrieve setting values.'; $string['notallow'] = 'Do not use this service'; $string['pluginname'] = 'Tool Settings Service'; $string['privacy:metadata'] = 'The Tool Settings Service plugin does not store any personal data.'; service/toolsettings/version.php000064400000002332152265520610013144 0ustar00. /** * Version information for the ltiservice_toolsettings service. * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_toolsettings'; $plugin->dependencies = [ 'ltiservice_profile' => 2025040800, 'ltiservice_toolproxy' => 2025040800, ]; service/toolsettings/classes/privacy/provider.php000064400000003027152265520610016425 0ustar00. /** * Privacy Subsystem implementation for ltiservice_toolsettings. * * @package ltiservice_toolsettings * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolsettings\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_toolsettings implementing null_provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } service/toolsettings/classes/local/service/toolsettings.php000064400000016420152265520610020407 0ustar00. /** * This file contains a class definition for the Tool Settings service * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolsettings\local\service; defined('MOODLE_INTERNAL') || die(); /** * A service implementing Tool Settings. * * @package ltiservice_toolsettings * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class toolsettings extends \mod_lti\local\ltiservice\service_base { /** Scope for managing tool settings */ const SCOPE_TOOL_SETTINGS = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting'; /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'toolsettings'; $this->name = 'Tool Settings'; } /** * Get the resources for this service. * * @return array */ public function get_resources() { if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new \ltiservice_toolsettings\local\resources\systemsettings($this); $this->resources[] = new \ltiservice_toolsettings\local\resources\contextsettings($this); $this->resources[] = new \ltiservice_toolsettings\local\resources\linksettings($this); } return $this->resources; } /** * Get the scope(s) permitted for the tool relevant to this service. * * @return array */ public function get_permitted_scopes() { $scopes = array(); $ok = !empty($this->get_type()); if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) && ($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) { $scopes[] = self::SCOPE_TOOL_SETTINGS; } return $scopes; } /** * Get the scope(s) defined this service. * * @return array */ public function get_scopes() { return [self::SCOPE_TOOL_SETTINGS]; } /** * Get the distinct settings from each level by removing any duplicates from higher levels. * * @param array $systemsettings System level settings * @param array $contextsettings Context level settings * @param array $linksettings Link level settings */ public static function distinct_settings(&$systemsettings, &$contextsettings, $linksettings) { if (!empty($systemsettings)) { foreach ($systemsettings as $key => $value) { if ((!empty($contextsettings) && array_key_exists($key, $contextsettings)) || (!empty($linksettings) && array_key_exists($key, $linksettings))) { unset($systemsettings[$key]); } } } if (!empty($contextsettings)) { foreach ($contextsettings as $key => $value) { if (!empty($linksettings) && array_key_exists($key, $linksettings)) { unset($contextsettings[$key]); } } } } /** * Get the JSON representation of the settings. * * @param array $settings Settings * @param boolean $simpleformat true if simple JSON is to be returned * @param string $type JSON-LD type * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * * @return string */ public static function settings_to_json($settings, $simpleformat, $type, $resource) { $json = ''; if (!empty($resource)) { $indent = ''; if (!$simpleformat) { $json .= " {\n \"@type\":\"{$type}\",\n"; $json .= " \"@id\":\"{$resource->get_endpoint()}\",\n"; $json .= " \"custom\":{"; $indent = ' '; } $isfirst = true; if (!empty($settings)) { foreach ($settings as $key => $value) { if (!$isfirst) { $json .= ','; } else { $isfirst = false; } $json .= "\n{$indent} \"{$key}\":\"{$value}\""; } } if (!$simpleformat) { $json .= "\n{$indent}}\n }"; } } return $json; } /** * Adds form elements for membership add/edit page. * * @param \MoodleQuickForm $mform */ public function get_configuration_options(&$mform) { $elementname = $this->get_component_id(); $options = [ get_string('notallow', $this->get_component_id()), get_string('allow', $this->get_component_id()) ]; $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options); $mform->setType($elementname, 'int'); $mform->setDefault($elementname, 0); $mform->addHelpButton($elementname, $elementname, $this->get_component_id()); } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid The course id. * @param string $user The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration * and not return parameters for services not used. * * @return array of key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { global $COURSE; $launchparameters = array(); $tool = lti_get_type_type_config($typeid); if (isset($tool->{$this->get_component_id()})) { if ($tool->{$this->get_component_id()} == self::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) { $launchparameters['system_setting_url'] = '$ToolProxy.custom.url'; $launchparameters['context_setting_url'] = '$ToolProxyBinding.custom.url'; if ($messagetype === 'basic-lti-launch-request') { $launchparameters['link_setting_url'] = '$LtiLink.custom.url'; } } } return $launchparameters; } } service/toolsettings/classes/local/resources/systemsettings.php000064400000014336152265520610021334 0ustar00. /** * This file contains a class definition for the System Settings resource * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolsettings\local\resources; use ltiservice_toolsettings\local\service\toolsettings; use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing the System-level (ToolProxy) Settings. * * @package ltiservice_toolsettings * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class systemsettings extends resource_base { /** * Class constructor. * * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolProxySettings'; $this->template = '/{config_type}/{tool_proxy_id}(/custom)'; $this->variables[] = 'ToolProxy.custom.url'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json'; $this->methods[] = self::HTTP_GET; $this->methods[] = self::HTTP_PUT; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { $params = $this->parse_template(); $tpid = $params['tool_proxy_id']; $configtype = $params['config_type']; $ok = (in_array($configtype, array('toolproxy', 'tool'))); if ($ok) { $typeid = null; if (($configtype === 'tool') && is_numeric($tpid)) { $typeid = $tpid; } $bubble = optional_param('bubble', '', PARAM_ALPHA); $ok = !empty($tpid) && $this->check_tool($typeid, $response->get_request_data(), array(toolsettings::SCOPE_TOOL_SETTINGS)); } if (!$ok) { $response->set_code(401); } else if (!empty($this->get_service()->get_tool_proxy())) { $ok = ($this->get_service()->get_tool_proxy()->guid === $tpid); $id = $this->get_service()->get_tool_proxy()->id; } else if (!empty($typeid)) { $id = -$typeid; } else { $ok = false; $response->set_code(404); } $contenttype = $response->get_accept(); $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]); if ($ok) { $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) && (!$simpleformat || empty($bubble) || ($bubble != 'all')) && (empty($bubble) || ($response->get_request_method() == 'GET')); if (!$ok) { $response->set_code(406); } } if ($ok) { $systemsettings = lti_get_tool_settings($id); if ($response->get_request_method() == 'GET') { $json = ''; if ($simpleformat) { $response->set_content_type($this->formats[1]); $json .= "{"; } else { $response->set_content_type($this->formats[0]); $json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n"; } $json .= toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $this); if ($simpleformat) { $json .= "\n}"; } else { $json .= "\n ]\n}"; } $response->set_body($json); } else { // PUT. $settings = null; if ($response->get_content_type() == $this->formats[0]) { $json = json_decode($response->get_request_data()); $ok = !empty($json); if ($ok) { $ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) && ($json->{"@graph"}[0]->{"@type"} == 'ToolProxy'); } if ($ok) { $settings = $json->{"@graph"}[0]->custom; unset($settings->{'@id'}); } } else { // Simple JSON. $json = json_decode($response->get_request_data(), true); $ok = !empty($json); if ($ok) { $ok = is_array($json); } if ($ok) { $settings = $json; } } if ($ok) { lti_set_tool_settings($settings, $id); } else { $response->set_code(406); } } } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { if (strpos($value, '$ToolProxy.custom.url') !== false) { $value = str_replace('$ToolProxy.custom.url', parent::get_endpoint(), $value); } return $value; } } service/toolsettings/classes/local/resources/linksettings.php000064400000021525152265520610020743 0ustar00. /** * This file contains a class definition for the Context Settings resource * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolsettings\local\resources; use ltiservice_toolsettings\local\service\toolsettings; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing the Context-level (ToolProxyBinding) Settings. * * @package ltiservice_toolsettings * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class linksettings extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'LtiLinkSettings'; $this->template = '/links/{link_id}(/custom)'; $this->variables[] = 'LtiLink.custom.url'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json'; $this->methods[] = 'GET'; $this->methods[] = 'PUT'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB, $COURSE; $params = $this->parse_template(); $linkid = $params['link_id']; $bubble = optional_param('bubble', '', PARAM_ALPHA); $contenttype = $response->get_accept(); $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]); $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) && (!$simpleformat || empty($bubble) || ($bubble != 'all')) && (empty($bubble) || ($response->get_request_method() == self::HTTP_GET)); if (!$ok) { $response->set_code(406); } $systemsetting = null; $contextsetting = null; $lti = null; if ($ok) { $ok = !empty($linkid); if ($ok) { $lti = $DB->get_record('lti', array('id' => $linkid), 'course,typeid', MUST_EXIST); $ok = $this->check_tool($lti->typeid, $response->get_request_data(), array(toolsettings::SCOPE_TOOL_SETTINGS)); } if (!$ok) { $response->set_code(401); } } if ($ok) { if (!empty($this->get_service()->get_tool_proxy())) { $id = $this->get_service()->get_tool_proxy()->id; } else { $id = -$this->get_service()->get_type()->id; } if ($response->get_request_method() == 'GET') { $linksettings = lti_get_tool_settings($id, $lti->course, $linkid); if (!empty($bubble)) { $contextsetting = new contextsettings($this->get_service()); if ($COURSE == 'site') { $contextsetting->params['context_type'] = 'Group'; } else { $contextsetting->params['context_type'] = 'CourseSection'; } $contextsetting->params['context_id'] = $lti->course; if ($id >= 0) { $contextsetting->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode; } else { $contextsetting->params['vendor_code'] = 'tool'; } $contextsetting->params['product_code'] = abs($id); $contextsettings = lti_get_tool_settings($id, $lti->course); $systemsetting = new systemsettings($this->get_service()); if ($id >= 0) { $systemsetting->params['config_type'] = 'toolproxy'; } else { $systemsetting->params['config_type'] = 'tool'; } $systemsetting->params['tool_proxy_id'] = abs($id); $systemsettings = lti_get_tool_settings($id); if ($bubble == 'distinct') { toolsettings::distinct_settings($systemsettings, $contextsettings, $linksettings); } } else { $contextsettings = null; $systemsettings = null; } $json = ''; if ($simpleformat) { $response->set_content_type($this->formats[1]); $json .= "{"; } else { $response->set_content_type($this->formats[0]); $json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n"; } $settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting); $json .= $settings; $isfirst = strlen($settings) <= 0; $settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $contextsetting); if (strlen($settings) > 0) { if (!$isfirst) { $json .= ","; if (!$simpleformat) { $json .= "\n"; } } $isfirst = false; } $json .= $settings; $settings = toolsettings::settings_to_json($linksettings, $simpleformat, 'LtiLink', $this); if ((strlen($settings) > 0) && !$isfirst) { $json .= ","; if (!$simpleformat) { $json .= "\n"; } } $json .= $settings; if ($simpleformat) { $json .= "\n}"; } else { $json .= "\n ]\n}"; } $response->set_body($json); } else { // PUT. $settings = null; if ($response->get_content_type() == $this->formats[0]) { $json = json_decode($response->get_request_data()); $ok = !empty($json); if ($ok) { $ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) && ($json->{"@graph"}[0]->{"@type"} == 'LtiLink'); } if ($ok) { $settings = $json->{"@graph"}[0]->custom; unset($settings->{'@id'}); } } else { // Simple JSON. $json = json_decode($response->get_request_data(), true); $ok = !empty($json); if ($ok) { $ok = is_array($json); } if ($ok) { $settings = $json; } } if ($ok) { lti_set_tool_settings($settings, $id, $lti->course, $linkid); } else { $response->set_code(406); } } } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { if (strpos($value, '$LtiLink.custom.url') !== false) { $id = optional_param('id', 0, PARAM_INT); // Course Module ID. if (!empty($id)) { $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); $this->params['link_id'] = $cm->instance; } $value = str_replace('$LtiLink.custom.url', parent::get_endpoint(), $value); } return $value; } } service/toolsettings/classes/local/resources/contextsettings.php000064400000020747152265520610021477 0ustar00. /** * This file contains a class definition for the Context Settings resource * * @package ltiservice_toolsettings * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_toolsettings\local\resources; use ltiservice_toolsettings\local\service\toolsettings; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing the Context-level (ToolProxyBinding) Settings. * * @package ltiservice_toolsettings * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextsettings extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolProxyBindingSettings'; $this->template = '/{context_type}/{context_id}/bindings/{vendor_code}/{product_code}(/custom)'; $this->variables[] = 'ToolProxyBinding.custom.url'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json'; $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json'; $this->methods[] = 'GET'; $this->methods[] = 'PUT'; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { $params = $this->parse_template(); $contexttype = $params['context_type']; $contextid = $params['context_id']; $vendorcode = $params['vendor_code']; $productcode = $params['product_code']; $bubble = optional_param('bubble', '', PARAM_ALPHA); $typeid = null; if (($vendorcode === 'tool') && is_numeric($productcode)) { $typeid = $productcode; } $ok = !empty($contexttype) && !empty($contextid) && !empty($vendorcode) && !empty($productcode) && $this->check_tool($typeid, $response->get_request_data(), array(toolsettings::SCOPE_TOOL_SETTINGS)); if (!$ok) { $response->set_code(401); } else { $toolproxy = $this->get_service()->get_tool_proxy(); if (!empty($toolproxy)) { $ok = $toolproxy->guid === $productcode; $typeid = null; $id = $toolproxy->id; } else { $ok = $vendorcode === 'tool'; $typeid = intval($productcode); $id = -$typeid; } $contenttype = $response->get_accept(); $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]); if ($ok) { $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) && (!$simpleformat || empty($bubble) || ($bubble != 'all')) && (empty($bubble) || ($response->get_request_method() == 'GET')); } if (!$ok) { $response->set_code(404); } else { $systemsetting = null; $contextsettings = lti_get_tool_settings($id, $contextid); if (!empty($bubble)) { $systemsetting = new systemsettings($this->get_service()); $systemsetting->params['tool_proxy_id'] = $productcode; if ($id >= 0) { $systemsetting->params['config_type'] = 'toolproxy'; } else { $systemsetting->params['config_type'] = 'tool'; } $systemsettings = lti_get_tool_settings($id); if ($bubble == 'distinct') { toolsettings::distinct_settings($systemsettings, $contextsettings, null); } } else { $systemsettings = null; } if ($response->get_request_method() == 'GET') { $json = ''; if ($simpleformat) { $response->set_content_type($this->formats[1]); $json .= "{"; } else { $response->set_content_type($this->formats[0]); $json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n"; } $settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting); $json .= $settings; $isfirst = strlen($settings) <= 0; $settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $this); if ((strlen($settings) > 0) && !$isfirst) { $json .= ","; } $json .= $settings; if ($simpleformat) { $json .= "\n}"; } else { $json .= "\n ]\n}"; } $response->set_body($json); } else { // PUT. $settings = null; if ($response->get_content_type() == $this->formats[0]) { $json = json_decode($response->get_request_data()); $ok = !empty($json); if ($ok) { $ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) && ($json->{"@graph"}[0]->{"@type"} == 'ToolProxyBinding'); } if ($ok) { $settings = $json->{"@graph"}[0]->custom; unset($settings->{'@id'}); } } else { // Simple JSON. $json = json_decode($response->get_request_data(), true); $ok = !empty($json); if ($ok) { $ok = is_array($json); } if ($ok) { $settings = $json; } } if ($ok) { lti_set_tool_settings($settings, $id, $contextid); } else { $response->set_code(406); } } } } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { global $COURSE; if (strpos($value, '$ToolProxyBinding.custom.url') !== false) { if ($COURSE->format == 'site') { $this->params['context_type'] = 'Group'; } else { $this->params['context_type'] = 'CourseSection'; } $this->params['context_id'] = $COURSE->id; if (!empty($this->get_service()->get_tool_proxy())) { $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode; $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid; } else { $this->params['vendor_code'] = 'tool'; $this->params['product_code'] = $this->get_service()->get_type()->id; } $value = str_replace('$ToolProxyBinding.custom.url', parent::get_endpoint(), $value); } return $value; } } service/profile/lang/en/ltiservice_profile.php000064400000002166152265520610015602 0ustar00. /** * Strings for component 'ltiservice_profile', language 'en' * * @package ltiservice_profile * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Tool Consumer Profile LTI Service'; $string['privacy:metadata'] = 'The Tool Consumer Profile LTI Service plugin does not store any personal data.'; service/profile/version.php000064400000002134152265520610012046 0ustar00. /** * Version information for the ltiservice_profile service. * * @package ltiservice_profile * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'ltiservice_profile'; service/profile/classes/privacy/provider.php000064400000003003152265520610015321 0ustar00. /** * Privacy Subsystem implementation for ltiservice_profile. * * @package ltiservice_profile * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_profile\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for ltiservice_profile implementing null_provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } service/profile/classes/local/service/profile.php000064400000003670152265520610016216 0ustar00. /** * This file contains a class definition for the Tool Consumer Profile service * * @package ltiservice_profile * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_profile\local\service; defined('MOODLE_INTERNAL') || die(); /** * A service implementing the Tool Consumer Profile. * * @package ltiservice_profile * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile extends \mod_lti\local\ltiservice\service_base { /** * Class constructor. */ public function __construct() { parent::__construct(); $this->id = 'profile'; $this->name = 'Tool Consumer Profile'; $this->unsigned = true; } /** * Get the resources for this service. * * @return array */ public function get_resources() { if (empty($this->resources)) { $this->resources = array(); $this->resources[] = new \ltiservice_profile\local\resources\profile($this); } return $this->resources; } } service/profile/classes/local/resources/profile.php000064400000016315152265520610016570 0ustar00. /** * This file contains a class definition for the Tool Consumer Profile resource * * @package ltiservice_profile * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_profile\local\resources; use \mod_lti\local\ltiservice\service_base; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing the Tool Consumer Profile. * * @package ltiservice_profile * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * * @param service_base $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolConsumerProfile'; $this->template = '/profile/{tool_proxy_id}'; $this->variables[] = 'ToolConsumerProfile.url'; $this->formats[] = 'application/vnd.ims.lti.v2.toolconsumerprofile+json'; $this->methods[] = 'GET'; } /** * Get the path for this resource. * * @return string */ public function get_path() { $path = $this->template; $toolproxy = $this->get_service()->get_tool_proxy(); if (!empty($toolproxy)) { $path = str_replace('{tool_proxy_id}', $toolproxy->guid, $path); } return $path; } /** * Execute the request for this resource. * * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $CFG; $version = service_base::LTI_VERSION2P0; $params = $this->parse_template(); if (optional_param('lti_version', service_base::LTI_VERSION2P0, PARAM_ALPHANUMEXT) != $version) { $ok = false; $response->set_code(400); } else { $toolproxy = lti_get_tool_proxy_from_guid($params['tool_proxy_id']); $ok = $toolproxy !== false; } if ($ok) { $this->get_service()->set_tool_proxy($toolproxy); $response->set_content_type($this->formats[0]); $servicepath = $this->get_service()->get_service_path(); $id = $servicepath . $this->get_path(); $now = date('Y-m-d\TH:iO'); $capabilityofferedarr = explode("\n", $toolproxy->capabilityoffered); $serviceofferedarr = explode("\n", $toolproxy->serviceoffered); $serviceoffered = ''; $sep = ''; $services = \core_component::get_plugin_list('ltiservice'); foreach ($services as $name => $location) { if (in_array($name, $serviceofferedarr)) { $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; /** @var service_base $service */ $service = new $classname(); $service->set_tool_proxy($toolproxy); $resources = $service->get_resources(); foreach ($resources as $resource) { $formats = implode("\", \"", $resource->get_formats()); $methods = implode("\", \"", $resource->get_methods()); $capabilityofferedarr = array_merge($capabilityofferedarr, $resource->get_variables()); $template = $resource->get_path(); if (!empty($template)) { $path = $servicepath . preg_replace('/[\(\)]/', '', $template); } else { $path = $resource->get_endpoint(); } $serviceoffered .= <<< EOD {$sep} { "@type":"{$resource->get_type()}", "@id":"tcp:{$resource->get_id()}", "endpoint":"{$path}", "format":["{$formats}"], "action":["{$methods}"] } EOD; $sep = ','; } } } $capabilityoffered = implode("\",\n \"", $capabilityofferedarr); if (strlen($capabilityoffered) > 0) { $capabilityoffered = "\n \"{$capabilityoffered}\""; } $urlparts = parse_url($CFG->wwwroot); $orgid = $urlparts['host']; $name = 'Moodle'; $code = 'moodle'; $vendorname = 'Moodle.org'; $vendorcode = 'mdl'; $prodversion = strval($CFG->version); if (!empty($CFG->mod_lti_institution_name)) { $consumername = $CFG->mod_lti_institution_name; $consumerdesc = ''; } else { $consumername = get_site()->fullname; $consumerdesc = strip_tags(get_site()->summary); } $profile = <<< EOD { "@context":[ "http://purl.imsglobal.org/ctx/lti/v2/ToolConsumerProfile", { "tcp":"{$id}#" } ], "@type":"ToolConsumerProfile", "@id":"{$id}", "lti_version":"{$version}", "guid":"{$toolproxy->guid}", "product_instance":{ "guid":"{$orgid}", "product_info":{ "product_name":{ "default_value":"{$name}", "key":"product.name" }, "product_version":"{$prodversion}", "product_family":{ "code":"{$code}", "vendor":{ "code":"{$vendorcode}", "vendor_name":{ "default_value":"{$vendorname}", "key":"product.vendor.name" }, "timestamp":"{$now}" } } }, "service_owner":{ "@id":"ServiceOwner", "service_owner_name":{ "default_value":"{$consumername}", "key":"service_owner.name" }, "description":{ "default_value":"{$consumerdesc}", "key":"service_owner.description" } } }, "capability_offered":[{$capabilityoffered} ], "service_offered":[{$serviceoffered} ] } EOD; $response->set_body($profile); } } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { if (!empty($this->get_service()->get_tool_proxy()) && (strpos($value, '$ToolConsumerProfile.url') !== false)) { $value = str_replace('$ToolConsumerProfile.url', $this->get_endpoint(), $value); } return $value; } } classes/external.php000064400000112071152265520610010542 0ustar00. /** * External tool module external API * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ use core_course\external\helper_for_get_mods_by_courses; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/mod/lti/lib.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * External tool module external functions * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ class mod_lti_external extends external_api { /** * Returns structure be used for returning a tool type from a web service. * * @return external_function_parameters * @since Moodle 3.1 */ private static function tool_type_return_structure() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Tool type id'), 'name' => new external_value(PARAM_NOTAGS, 'Tool type name'), 'description' => new external_value(PARAM_NOTAGS, 'Tool type description'), 'platformid' => new external_value(PARAM_TEXT, 'Platform ID'), 'clientid' => new external_value(PARAM_TEXT, 'Client ID'), 'deploymentid' => new external_value(PARAM_INT, 'Deployment ID'), 'urls' => new external_single_structure( array( 'icon' => new external_value(PARAM_URL, 'Tool type icon URL'), 'edit' => new external_value(PARAM_URL, 'Tool type edit URL'), 'course' => new external_value(PARAM_URL, 'Tool type edit URL', VALUE_OPTIONAL), 'publickeyset' => new external_value(PARAM_URL, 'Public Keyset URL'), 'accesstoken' => new external_value(PARAM_URL, 'Access Token URL'), 'authrequest' => new external_value(PARAM_URL, 'Authorisation Request URL'), ) ), 'state' => new external_single_structure( array( 'text' => new external_value(PARAM_TEXT, 'Tool type state name string'), 'pending' => new external_value(PARAM_BOOL, 'Is the state pending'), 'configured' => new external_value(PARAM_BOOL, 'Is the state configured'), 'rejected' => new external_value(PARAM_BOOL, 'Is the state rejected'), 'unknown' => new external_value(PARAM_BOOL, 'Is the state unknown'), ) ), 'hascapabilitygroups' => new external_value(PARAM_BOOL, 'Indicate if capabilitygroups is populated'), 'capabilitygroups' => new external_multiple_structure( new external_value(PARAM_TEXT, 'Tool type capability groups enabled'), 'Array of capability groups', VALUE_DEFAULT, array() ), 'courseid' => new external_value(PARAM_INT, 'Tool type course', VALUE_DEFAULT, 0), 'instanceids' => new external_multiple_structure( new external_value(PARAM_INT, 'LTI instance ID'), 'IDs for the LTI instances using this type', VALUE_DEFAULT, array() ), 'instancecount' => new external_value(PARAM_INT, 'The number of times this tool is being used') ), 'Tool' ); } /** * Returns description of a tool proxy * * @return external_function_parameters * @since Moodle 3.1 */ private static function tool_proxy_return_structure() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool proxy id'), 'name' => new external_value(PARAM_TEXT, 'Tool proxy name'), 'regurl' => new external_value(PARAM_URL, 'Tool proxy registration URL'), 'state' => new external_value(PARAM_INT, 'Tool proxy state'), 'guid' => new external_value(PARAM_TEXT, 'Tool proxy globally unique identifier'), 'secret' => new external_value(PARAM_TEXT, 'Tool proxy shared secret'), 'vendorcode' => new external_value(PARAM_TEXT, 'Tool proxy consumer code'), 'capabilityoffered' => new external_value(PARAM_TEXT, 'Tool proxy capabilities offered'), 'serviceoffered' => new external_value(PARAM_TEXT, 'Tool proxy services offered'), 'toolproxy' => new external_value(PARAM_TEXT, 'Tool proxy'), 'timecreated' => new external_value(PARAM_INT, 'Tool proxy time created'), 'timemodified' => new external_value(PARAM_INT, 'Tool proxy modified'), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_tool_proxies_parameters() { return new external_function_parameters( array( 'orphanedonly' => new external_value(PARAM_BOOL, 'Orphaned tool types only', VALUE_DEFAULT, 0) ) ); } /** * Returns the tool types. * * @param bool $orphanedonly Retrieve only tool proxies that do not have a corresponding tool type * @return array of tool types * @since Moodle 3.1 * @throws moodle_exception */ public static function get_tool_proxies($orphanedonly) { $params = self::validate_parameters(self::get_tool_proxies_parameters(), array( 'orphanedonly' => $orphanedonly )); $orphanedonly = $params['orphanedonly']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); return lti_get_tool_proxies($orphanedonly); } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_tool_proxies_returns() { return new external_multiple_structure( self::tool_proxy_return_structure() ); } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_tool_launch_data_parameters() { return new external_function_parameters( array( 'toolid' => new external_value(PARAM_INT, 'external tool instance id') ) ); } /** * Return the launch data for a given external tool. * * @param int $toolid the external tool instance id * @return array of warnings and launch data * @since Moodle 3.0 * @throws moodle_exception */ public static function get_tool_launch_data($toolid) { global $DB, $CFG; require_once($CFG->dirroot . '/mod/lti/lib.php'); $params = self::validate_parameters(self::get_tool_launch_data_parameters(), array( 'toolid' => $toolid )); $warnings = array(); // Request and permission validation. $lti = $DB->get_record('lti', array('id' => $params['toolid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($lti, 'lti'); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/lti:view', $context); $lti->cmid = $cm->id; list($endpoint, $parms) = lti_get_launch_data($lti); $parameters = array(); foreach ($parms as $name => $value) { $parameters[] = array( 'name' => $name, 'value' => $value ); } $result = array(); $result['endpoint'] = $endpoint; $result['parameters'] = $parameters; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function get_tool_launch_data_returns() { return new external_single_structure( array( 'endpoint' => new external_value(PARAM_RAW, 'Endpoint URL'), // Using PARAM_RAW as is defined in the module. 'parameters' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_NOTAGS, 'Parameter name'), 'value' => new external_value(PARAM_RAW, 'Parameter value') ) ) ), 'warnings' => new external_warnings() ) ); } /** * Describes the parameters for get_ltis_by_courses. * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_ltis_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array() ), ) ); } /** * Returns a list of external tools in a provided list of courses, * if no list is provided all external tools that the user can view will be returned. * * @param array $courseids the course ids * @return array the lti details * @since Moodle 3.0 */ public static function get_ltis_by_courses($courseids = array()) { global $CFG; $returnedltis = array(); $warnings = array(); $params = self::validate_parameters(self::get_ltis_by_courses_parameters(), array('courseids' => $courseids)); $mycourses = array(); if (empty($params['courseids'])) { $mycourses = enrol_get_my_courses(); $params['courseids'] = array_keys($mycourses); } // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { list($courses, $warnings) = util::validate_courses($params['courseids'], $mycourses); // Get the ltis in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $ltis = get_all_instances_in_courses("lti", $courses); foreach ($ltis as $lti) { $context = context_module::instance($lti->coursemodule); // Entry to return. $module = helper_for_get_mods_by_courses::standard_coursemodule_element_values( $lti, 'mod_lti', 'moodle/course:manageactivities', 'mod/lti:view'); $viewablefields = []; if (has_capability('mod/lti:view', $context)) { $viewablefields = array('launchcontainer', 'showtitlelaunch', 'showdescriptionlaunch', 'icon', 'secureicon'); } // Check additional permissions for returning optional private settings. if (has_capability('moodle/course:manageactivities', $context)) { $additionalfields = array('timecreated', 'timemodified', 'typeid', 'toolurl', 'securetoolurl', 'instructorchoicesendname', 'instructorchoicesendemailaddr', 'instructorchoiceallowroster', 'instructorchoiceallowsetting', 'instructorcustomparameters', 'instructorchoiceacceptgrades', 'grade', 'resourcekey', 'password', 'debuglaunch', 'servicesalt'); $viewablefields = array_merge($viewablefields, $additionalfields); } foreach ($viewablefields as $field) { $module[$field] = $lti->{$field}; } $returnedltis[] = $module; } } $result = array(); $result['ltis'] = $returnedltis; $result['warnings'] = $warnings; return $result; } /** * Describes the get_ltis_by_courses return value. * * @return external_single_structure * @since Moodle 3.0 */ public static function get_ltis_by_courses_returns() { return new external_single_structure( array( 'ltis' => new external_multiple_structure( new external_single_structure(array_merge( helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(true), [ 'timecreated' => new external_value(PARAM_INT, 'Time of creation', VALUE_OPTIONAL), 'timemodified' => new external_value(PARAM_INT, 'Time of last modification', VALUE_OPTIONAL), 'typeid' => new external_value(PARAM_INT, 'Type id', VALUE_OPTIONAL), 'toolurl' => new external_value(PARAM_URL, 'Tool url', VALUE_OPTIONAL), 'securetoolurl' => new external_value(PARAM_RAW, 'Secure tool url', VALUE_OPTIONAL), 'instructorchoicesendname' => new external_value(PARAM_TEXT, 'Instructor choice send name', VALUE_OPTIONAL), 'instructorchoicesendemailaddr' => new external_value(PARAM_INT, 'instructor choice send mail address', VALUE_OPTIONAL), 'instructorchoiceallowroster' => new external_value(PARAM_INT, 'Instructor choice allow roster', VALUE_OPTIONAL), 'instructorchoiceallowsetting' => new external_value(PARAM_INT, 'Instructor choice allow setting', VALUE_OPTIONAL), 'instructorcustomparameters' => new external_value(PARAM_RAW, 'instructor custom parameters', VALUE_OPTIONAL), 'instructorchoiceacceptgrades' => new external_value(PARAM_INT, 'instructor choice accept grades', VALUE_OPTIONAL), 'grade' => new external_value(PARAM_INT, 'Enable grades', VALUE_OPTIONAL), 'launchcontainer' => new external_value(PARAM_INT, 'Launch container mode', VALUE_OPTIONAL), 'resourcekey' => new external_value(PARAM_RAW, 'Resource key', VALUE_OPTIONAL), 'password' => new external_value(PARAM_RAW, 'Shared secret', VALUE_OPTIONAL), 'debuglaunch' => new external_value(PARAM_INT, 'Debug launch', VALUE_OPTIONAL), 'showtitlelaunch' => new external_value(PARAM_INT, 'Show title launch', VALUE_OPTIONAL), 'showdescriptionlaunch' => new external_value(PARAM_INT, 'Show description launch', VALUE_OPTIONAL), 'servicesalt' => new external_value(PARAM_RAW, 'Service salt', VALUE_OPTIONAL), 'icon' => new external_value(PARAM_URL, 'Alternative icon URL', VALUE_OPTIONAL), 'secureicon' => new external_value(PARAM_URL, 'Secure icon URL', VALUE_OPTIONAL), ] ), 'Tool') ), 'warnings' => new external_warnings(), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function view_lti_parameters() { return new external_function_parameters( array( 'ltiid' => new external_value(PARAM_INT, 'lti instance id') ) ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $ltiid the lti instance id * @return array of warnings and status result * @since Moodle 3.0 * @throws moodle_exception */ public static function view_lti($ltiid) { global $DB; $params = self::validate_parameters(self::view_lti_parameters(), array( 'ltiid' => $ltiid )); $warnings = array(); // Request and permission validation. $lti = $DB->get_record('lti', array('id' => $params['ltiid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($lti, 'lti'); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/lti:view', $context); // Trigger course_module_viewed event and completion. lti_view($lti, $course, $cm, $context); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function view_lti_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function create_tool_proxy_parameters() { return new external_function_parameters( array( 'name' => new external_value(PARAM_TEXT, 'Tool proxy name', VALUE_DEFAULT, ''), 'regurl' => new external_value(PARAM_URL, 'Tool proxy registration URL'), 'capabilityoffered' => new external_multiple_structure( new external_value(PARAM_TEXT, 'Tool proxy capabilities offered'), 'Array of capabilities', VALUE_DEFAULT, array() ), 'serviceoffered' => new external_multiple_structure( new external_value(PARAM_TEXT, 'Tool proxy services offered'), 'Array of services', VALUE_DEFAULT, array() ) ) ); } /** * Creates a new tool proxy * * @param string $name Tool proxy name * @param string $registrationurl Registration url * @param string[] $capabilityoffered List of capabilities this tool proxy should be offered * @param string[] $serviceoffered List of services this tool proxy should be offered * @return object The new tool proxy * @since Moodle 3.1 * @throws moodle_exception */ public static function create_tool_proxy($name, $registrationurl, $capabilityoffered, $serviceoffered) { $params = self::validate_parameters(self::create_tool_proxy_parameters(), array( 'name' => $name, 'regurl' => $registrationurl, 'capabilityoffered' => $capabilityoffered, 'serviceoffered' => $serviceoffered )); $name = $params['name']; $regurl = $params['regurl']; $capabilityoffered = $params['capabilityoffered']; $serviceoffered = $params['serviceoffered']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); // Can't create duplicate proxies with the same URL. $duplicates = lti_get_tool_proxies_from_registration_url($registrationurl); if (!empty($duplicates)) { throw new moodle_exception('duplicateregurl', 'mod_lti'); } $config = new stdClass(); $config->lti_registrationurl = $registrationurl; if (!empty($name)) { $config->lti_registrationname = $name; } if (!empty($capabilityoffered)) { $config->lti_capabilities = $capabilityoffered; } if (!empty($serviceoffered)) { $config->lti_services = $serviceoffered; } $id = lti_add_tool_proxy($config); $toolproxy = lti_get_tool_proxy($id); // Pending makes more sense than configured as the first state, since // the next step is to register, which requires the state be pending. $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING; lti_update_tool_proxy($toolproxy); return $toolproxy; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function create_tool_proxy_returns() { return self::tool_proxy_return_structure(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function delete_tool_proxy_parameters() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool proxy id'), ) ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $id the lti instance id * @return object The tool proxy * @since Moodle 3.1 * @throws moodle_exception */ public static function delete_tool_proxy($id) { $params = self::validate_parameters(self::delete_tool_proxy_parameters(), array( 'id' => $id, )); $id = $params['id']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $toolproxy = lti_get_tool_proxy($id); lti_delete_tool_proxy($id); return $toolproxy; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function delete_tool_proxy_returns() { return self::tool_proxy_return_structure(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function get_tool_proxy_registration_request_parameters() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool proxy id'), ) ); } /** * Returns the registration request for a tool proxy. * * @param int $id the lti instance id * @return array of registration parameters * @since Moodle 3.1 * @throws moodle_exception */ public static function get_tool_proxy_registration_request($id) { $params = self::validate_parameters(self::get_tool_proxy_registration_request_parameters(), array( 'id' => $id, )); $id = $params['id']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $toolproxy = lti_get_tool_proxy($id); return lti_build_registration_request($toolproxy); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_tool_proxy_registration_request_returns() { return new external_function_parameters( array( 'lti_message_type' => new external_value(PARAM_ALPHANUMEXT, 'LTI message type'), 'lti_version' => new external_value(PARAM_ALPHANUMEXT, 'LTI version'), 'reg_key' => new external_value(PARAM_TEXT, 'Tool proxy registration key'), 'reg_password' => new external_value(PARAM_TEXT, 'Tool proxy registration password'), 'reg_url' => new external_value(PARAM_TEXT, 'Tool proxy registration url'), 'tc_profile_url' => new external_value(PARAM_URL, 'Tool consumers profile URL'), 'launch_presentation_return_url' => new external_value(PARAM_URL, 'URL to redirect on registration completion'), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_tool_types_parameters() { return new external_function_parameters( array( 'toolproxyid' => new external_value(PARAM_INT, 'Tool proxy id', VALUE_DEFAULT, 0) ) ); } /** * Returns the tool types. * * @param int $toolproxyid The tool proxy id * @return array of tool types * @since Moodle 3.1 * @throws moodle_exception */ public static function get_tool_types($toolproxyid) { global $PAGE; $params = self::validate_parameters(self::get_tool_types_parameters(), array( 'toolproxyid' => $toolproxyid )); $toolproxyid = $params['toolproxyid']; $types = array(); $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); if (!empty($toolproxyid)) { $types = lti_get_lti_types_from_proxy_id($toolproxyid); } else { $types = lti_get_lti_types(); } return array_map("serialise_tool_type", array_values($types)); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_tool_types_returns() { return new external_multiple_structure( self::tool_type_return_structure() ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function create_tool_type_parameters() { return new external_function_parameters( array( 'cartridgeurl' => new external_value(PARAM_URL, 'URL to cardridge to load tool information', VALUE_DEFAULT, ''), 'key' => new external_value(PARAM_TEXT, 'Consumer key', VALUE_DEFAULT, ''), 'secret' => new external_value(PARAM_TEXT, 'Shared secret', VALUE_DEFAULT, ''), ) ); } /** * Creates a tool type. * * @param string $cartridgeurl Url of the xml cartridge representing the LTI tool * @param string $key The consumer key to identify this consumer * @param string $secret The secret * @return array created tool type * @since Moodle 3.1 * @throws moodle_exception If the tool type could not be created */ public static function create_tool_type($cartridgeurl, $key, $secret) { $params = self::validate_parameters(self::create_tool_type_parameters(), array( 'cartridgeurl' => $cartridgeurl, 'key' => $key, 'secret' => $secret )); $cartridgeurl = $params['cartridgeurl']; $key = $params['key']; $secret = $params['secret']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $id = null; if (!empty($cartridgeurl)) { $type = new stdClass(); $data = new stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $data->lti_coursevisible = 1; $data->lti_sendname = LTI_SETTING_DELEGATE; $data->lti_sendemailaddr = LTI_SETTING_DELEGATE; $data->lti_acceptgrades = LTI_SETTING_DELEGATE; $data->lti_forcessl = 0; if (!empty($key)) { $data->lti_resourcekey = $key; } if (!empty($secret)) { $data->lti_password = $secret; } lti_load_type_from_cartridge($cartridgeurl, $data); if (empty($data->lti_toolurl)) { throw new moodle_exception('unabletocreatetooltype', 'mod_lti'); } else { $id = lti_add_type($type, $data); } } if (!empty($id)) { $type = lti_get_type($id); return serialise_tool_type($type); } else { throw new moodle_exception('unabletocreatetooltype', 'mod_lti'); } } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function create_tool_type_returns() { return self::tool_type_return_structure(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function update_tool_type_parameters() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool type id'), 'name' => new external_value(PARAM_RAW, 'Tool type name', VALUE_DEFAULT, null), 'description' => new external_value(PARAM_RAW, 'Tool type description', VALUE_DEFAULT, null), 'state' => new external_value(PARAM_INT, 'Tool type state', VALUE_DEFAULT, null) ) ); } /** * Update a tool type. * * @param int $id The id of the tool type to update * @param string $name The name of the tool type * @param string $description The name of the tool type * @param int $state The state of the tool type * @return array updated tool type * @since Moodle 3.1 * @throws moodle_exception */ public static function update_tool_type($id, $name, $description, $state) { $params = self::validate_parameters(self::update_tool_type_parameters(), array( 'id' => $id, 'name' => $name, 'description' => $description, 'state' => $state, )); $id = $params['id']; $name = $params['name']; $description = $params['description']; $state = $params['state']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $type = lti_get_type($id); if (empty($type)) { throw new moodle_exception('unabletofindtooltype', 'mod_lti', '', array('id' => $id)); } if (!empty($name)) { $type->name = $name; } if (!empty($description)) { $type->description = $description; } if (!empty($state)) { // Valid state range. if (in_array($state, array(1, 2, 3))) { $type->state = $state; } else { throw new moodle_exception("Invalid state: $state - must be 1, 2, or 3"); } } lti_update_type($type, new stdClass()); return serialise_tool_type($type); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function update_tool_type_returns() { return self::tool_type_return_structure(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function delete_tool_type_parameters() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool type id'), ) ); } /** * Delete a tool type. * * @param int $id The id of the tool type to be deleted * @return array deleted tool type * @since Moodle 3.1 * @throws moodle_exception */ public static function delete_tool_type($id) { $params = self::validate_parameters(self::delete_tool_type_parameters(), array( 'id' => $id, )); $id = $params['id']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $type = lti_get_type($id); if (!empty($type)) { lti_delete_type($id); // If this is the last type for this proxy then remove the proxy // as well so that it isn't orphaned. $types = lti_get_lti_types_from_proxy_id($type->toolproxyid); if (empty($types)) { lti_delete_tool_proxy($type->toolproxyid); } } return array('id' => $id); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function delete_tool_type_returns() { return new external_function_parameters( array( 'id' => new external_value(PARAM_INT, 'Tool type id'), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function is_cartridge_parameters() { return new external_function_parameters( array( 'url' => new external_value(PARAM_URL, 'Tool url'), ) ); } /** * Determine if the url to a tool is for a cartridge. * * @param string $url Url that may or may not be an xml cartridge * @return bool True if the url is for a cartridge. * @since Moodle 3.1 * @throws moodle_exception */ public static function is_cartridge($url) { $params = self::validate_parameters(self::is_cartridge_parameters(), array( 'url' => $url, )); $url = $params['url']; $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $iscartridge = lti_is_cartridge($url); return array('iscartridge' => $iscartridge); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function is_cartridge_returns() { return new external_function_parameters( array( 'iscartridge' => new external_value(PARAM_BOOL, 'True if the URL is a cartridge'), ) ); } } classes/reportbuilder/local/entities/tool_types.php000064400000013461152265520610016724 0ustar00. namespace mod_lti\reportbuilder\local\entities; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Course external tools entity class implementation. * * Defines all the columns and filters that can be added to reports that use this entity. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_types extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'lti_types', 'lti', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('entitycourseexternaltools', 'mod_lti'); } /** * Initialize the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this->add_filter($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('lti_types'); // Name column. $columns[] = (new column( 'name', new lang_string('name', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.icon") ->set_is_sortable(true) ->add_callback(static function(string $name, \stdClass $data) { global $OUTPUT; $iconurl = $data->icon ?: $OUTPUT->image_url('monologo', 'lti')->out(); $iconclass = $data->icon ? ' nofilter' : ''; $iconcontainerclass = 'activityiconcontainer smaller'; $name = $data->name; $img = \html_writer::img($iconurl, get_string('courseexternaltooliconalt', 'mod_lti', $name), ['class' => 'activityicon' . $iconclass]); $name = \html_writer::span($name, 'align-self-center'); return \html_writer::div(\html_writer::div($img, 'me-2 '.$iconcontainerclass) . $name, 'd-flex'); }); // Description column. $columns[] = (new column( 'description', new lang_string('description', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.description") ->set_is_sortable(true); // Course column. $columns[] = (new column( 'course', new lang_string('course', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("{$tablealias}.course") ->set_is_sortable(true); // LTI Version column. $columns[] = (new column( 'ltiversion', new lang_string('version'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.ltiversion") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('lti_types'); return [ // Name filter. (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()), // Description filter. (new filter( text::class, 'description', new lang_string('description'), $this->get_entity_name(), "{$tablealias}.description" )) ->add_joins($this->get_joins()), // LTI Version filter. (new filter( select::class, 'ltiversion', new lang_string('version'), $this->get_entity_name(), "{$tablealias}.ltiversion" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { return ['LTI-1p0' => 'Legacy LTI', '1.3.0' => "LTI Advantage"]; }) ]; } } classes/reportbuilder/local/systemreports/course_external_tools_list.php000064400000026760152265520610023325 0ustar00. namespace mod_lti\reportbuilder\local\systemreports; use core\output\html_writer; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\column; use mod_lti\reportbuilder\local\entities\tool_types; use core_reportbuilder\system_report; /** * Course external tools list system report class implementation. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_external_tools_list extends system_report { /** @var \stdClass the course to constrain the report to. */ protected \stdClass $course; /** @var int the usage count for the tool represented in a row, and set by row_callback(). */ protected int $perrowtoolusage = 0; /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { global $DB, $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $this->course = get_course($this->get_context()->instanceid); // Our main entity, it contains all the column definitions that we need. $entitymain = new tool_types(); $entitymainalias = $entitymain->get_table_alias('lti_types'); $this->set_main_table('lti_types', $entitymainalias); $this->add_entity($entitymain); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns($entitymain); $this->add_filters(); $this->add_actions(); // We need id and course in the actions column, without entity prefixes, so add these here. // We also need access to the tool usage count in a few places (the usage column as well as the actions column). $ti = database::generate_param_name(); // Tool instance param. $this->add_base_fields("{$entitymainalias}.id, {$entitymainalias}.course, ". "(SELECT COUNT($ti.id) FROM {lti} $ti WHERE $ti.typeid = {$entitymainalias}.id) AS toolusage"); // Join the types_categories table, to include only tools available to the current course's category. $cattablealias = database::generate_alias(); $joinsql = "LEFT JOIN {lti_types_categories} {$cattablealias} ON ({$cattablealias}.typeid = {$entitymainalias}.id)"; $this->add_join($joinsql); // Scope the report to the course context and include only those tools available to the category. $paramprefix = database::generate_param_name(); $coursevisibleparam = database::generate_param_name(); $categoryparam = database::generate_param_name(); $toolstateparam = database::generate_param_name(); [$insql, $params] = $DB->get_in_or_equal([get_site()->id, $this->course->id], SQL_PARAMS_NAMED, "{$paramprefix}_"); $wheresql = "{$entitymainalias}.course {$insql} ". "AND {$entitymainalias}.coursevisible NOT IN (:{$coursevisibleparam}) ". "AND ({$cattablealias}.id IS NULL OR {$cattablealias}.categoryid = :{$categoryparam}) ". "AND {$entitymainalias}.state = :{$toolstateparam}"; $params = array_merge( $params, [ $coursevisibleparam => LTI_COURSEVISIBLE_NO, $categoryparam => $this->course->category, $toolstateparam => LTI_TOOL_STATE_CONFIGURED ] ); $this->add_base_condition_sql($wheresql, $params); $this->set_downloadable(false, get_string('pluginname', 'mod_lti')); $this->set_default_per_page(10); $this->set_default_no_results_notice(null); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_capability('mod/lti:addpreconfiguredinstance', $this->get_context()); } public function row_callback(\stdClass $row): void { $this->perrowtoolusage = $row->toolusage; } /** * Adds the columns we want to display in the report. * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier * @param tool_types $tooltypesentity * @return void */ protected function add_columns(tool_types $tooltypesentity): void { $entitymainalias = $tooltypesentity->get_table_alias('lti_types'); $columns = [ 'tool_types:name', 'tool_types:description', ]; $this->add_columns_from_entities($columns); // Tool usage column using a custom SQL subquery (defined in initialise method) to count tool instances within the course. // TODO: This should be replaced with proper column aggregation once that's added to system_report instances in MDL-76392. $this->add_column(new column( 'usage', new \lang_string('usage', 'mod_lti'), $tooltypesentity->get_entity_name() )) ->set_type(column::TYPE_INTEGER) ->set_is_sortable(true) ->add_field("{$entitymainalias}.id") ->add_callback(fn() => $this->perrowtoolusage); // Enable toggle column. $this->add_column((new column( 'showinactivitychooser', new \lang_string('showinactivitychooser', 'mod_lti'), $tooltypesentity->get_entity_name() )) // Site tools can be overridden on course level. ->add_join("LEFT JOIN {lti_coursevisible} lc ON lc.typeid = {$entitymainalias}.id AND lc.courseid = " . $this->course->id) ->set_type(column::TYPE_INTEGER) ->add_fields("{$entitymainalias}.id, {$entitymainalias}.coursevisible, lc.coursevisible as coursevisibleoverridden") ->set_is_sortable(false) ->set_callback(function(int $id, \stdClass $row): string { global $PAGE; $coursevisible = $row->coursevisible; $courseid = $this->course->id; if (!empty($row->coursevisibleoverridden)) { $coursevisible = $row->coursevisibleoverridden; } if ($coursevisible == LTI_COURSEVISIBLE_ACTIVITYCHOOSER) { $coursevisible = true; } else { $coursevisible = false; } $renderer = $PAGE->get_renderer('core_reportbuilder'); $attributes = [ ['name' => 'id', 'value' => $row->id], ['name' => 'courseid', 'value' => $courseid], ['name' => 'action', 'value' => 'showinactivitychooser-toggle'], ['name' => 'state', 'value' => $coursevisible], ]; $label = $coursevisible ? get_string('dontshowinactivitychooser', 'mod_lti') : get_string('showinactivitychooser', 'mod_lti'); $disabled = !has_capability('mod/lti:addcoursetool', \context_course::instance($courseid)); return $renderer->render_from_template('core/toggle', [ 'id' => 'showinactivitychooser-toggle-' . $row->id, 'checked' => $coursevisible, 'disabled' => $disabled, 'dataattributes' => $attributes, 'label' => $label, 'labelclasses' => 'visually-hidden', ]); }) ); // Attempt to create a dummy actions column, working around the limitations of the official actions feature. $this->add_column(new column( 'actions', new \lang_string('actions'), $tooltypesentity->get_entity_name() )) ->set_type(column::TYPE_TEXT) ->set_is_sortable(false) ->add_fields("{$entitymainalias}.id, {$entitymainalias}.course, {$entitymainalias}.name") ->add_callback(function($field, $row) { global $OUTPUT; // Lock actions for site-level preconfigured tools. if (get_site()->id == $row->course) { return \html_writer::div( \html_writer::div( $OUTPUT->pix_icon('t/locked', get_string('courseexternaltoolsnoeditpermissions', 'mod_lti') ), 'tool-action-icon-container'), 'd-flex justify-content-end' ); } // Lock actions when the user can't add course tools. if (!has_capability('mod/lti:addcoursetool', \context_course::instance($row->course))) { return \html_writer::div( \html_writer::div( $OUTPUT->pix_icon('t/locked', get_string('courseexternaltoolsnoeditpermissions', 'mod_lti') ), 'tool-action-icon-container'), 'd-flex justify-content-end' ); } // Build and display an action menu. $menu = new \action_menu(); $triggerlabel = get_string('actions'); $visuallyhiddenlabel = html_writer::span($triggerlabel, 'visually-hidden d-inline-block'); $menu->set_menu_trigger($OUTPUT->pix_icon('i/moremenu', '') . $visuallyhiddenlabel, 'btn btn-icon d-flex'); $menu->triggerattributes['title'] = $triggerlabel; $menu->add(new \action_menu_link( new \moodle_url('/mod/lti/coursetooledit.php', ['course' => $row->course, 'typeid' => $row->id]), null, get_string('edit', 'core'), null )); $menu->add(new \action_menu_link( new \moodle_url('#'), null, get_string('delete', 'core'), null, [ 'data-action' => 'course-tool-delete', 'data-course-tool-id' => $row->id, 'data-course-tool-name' => $row->name, 'data-course-tool-usage' => $this->perrowtoolusage, 'class' => 'text-danger', ], )); return $OUTPUT->render($menu); }); // Default sorting. $this->set_initial_sort_column('tool_types:name', SORT_ASC); } /** * Add any actions for this report. * * @return void */ protected function add_actions(): void { } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $this->add_filters_from_entities([]); } } classes/external/get_tool_types_and_proxies.php000064400000010355152265520610016177 0ustar00. namespace mod_lti\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; /** * External function for fetching all tool types and proxies. * * @package mod_lti * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_tool_types_and_proxies extends external_api { /** * Get parameter definition for get_tool_types_and_proxies(). * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'toolproxyid' => new external_value( PARAM_INT, 'Tool proxy id', VALUE_DEFAULT, 0 ), 'orphanedonly' => new external_value( PARAM_BOOL, 'Orphaned tool types only', VALUE_DEFAULT, 0 ), 'limit' => new external_value( PARAM_INT, 'How many tool types displayed per page', VALUE_DEFAULT, 60, NULL_NOT_ALLOWED ), 'offset' => new external_value( PARAM_INT, 'Current offset of tool elements', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED ), ]); } /** * Get data for all tool types and tool proxies. * * @param int $toolproxyid The tool proxy id * @param bool $orphanedonly Whether to get orphaned proxies only. * @param int $limit How many elements to return if using pagination. * @param int $offset Which chunk of elements to return is using pagination. * @return array */ public static function execute($toolproxyid, $orphanedonly, $limit, $offset): array { $params = self::validate_parameters(self::execute_parameters(), [ 'toolproxyid' => $toolproxyid, 'orphanedonly' => $orphanedonly, 'limit' => $limit, 'offset' => $offset, ]); $toolproxyid = $params['toolproxyid'] !== null ? $params['toolproxyid'] : 0; $orphanedonly = $params['orphanedonly'] !== null ? $params['orphanedonly'] : false; $limit = $params['limit'] !== null ? $params['limit'] : 0; $offset = $params['offset'] !== null ? $params['offset'] : 0; $context = \context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); [$proxies, $types] = lti_get_lti_types_and_proxies($limit, $offset, $orphanedonly, $toolproxyid); return [ 'types' => $types, 'proxies' => $proxies, 'limit' => $limit, 'offset' => $offset, ]; } /** * Get return definition for get_tool_types_and_proxies. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'types' => \mod_lti_external::get_tool_types_returns(), 'proxies' => \mod_lti_external::get_tool_proxies_returns(), 'limit' => new external_value(PARAM_INT, 'Limit of how many tool types to show', VALUE_OPTIONAL), 'offset' => new external_value(PARAM_INT, 'Offset of tool types', VALUE_OPTIONAL), ]); } } classes/external/delete_course_tool_type.php000064400000005240152265520610015461 0ustar00. namespace mod_lti\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * External function to delete a course tool type. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_course_tool_type extends external_api { /** * Get parameter definition. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'tooltypeid' => new external_value(PARAM_INT, 'Tool type ID'), ]); } /** * Delete a course tool type. * * @param int $tooltypeid the id of the course external tool type. * @return bool true * @throws \invalid_parameter_exception if the provided id refers to a site level tool which cannot be deleted. */ public static function execute(int $tooltypeid): bool { ['tooltypeid' => $tooltypeid] = self::validate_parameters(self::execute_parameters(), ['tooltypeid' => $tooltypeid]); global $DB; $course = (int) $DB->get_field('lti_types', 'course', ['id' => $tooltypeid]); if ($course == get_site()->id) { throw new \invalid_parameter_exception('This is a site-level tool and cannot be deleted via this service'); } $context = \context_course::instance($course); self::validate_context($context); require_capability('mod/lti:addcoursetool', $context); \lti_delete_type($tooltypeid); return true; } /** * Get service returns definition. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_BOOL, 'Success'); } } classes/external/toggle_showinactivitychooser.php000064400000005667152265520610016566 0ustar00. namespace mod_lti\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_value; use mod_lti\local\types_helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * External function to toggle showinactivitychooser setting. * * @package mod_lti * @copyright 2023 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class toggle_showinactivitychooser extends external_api { /** * Get parameter definition. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'tooltypeid' => new external_value(PARAM_INT, 'Tool type ID'), 'courseid' => new external_value(PARAM_INT, 'Course ID'), 'showinactivitychooser' => new external_value(PARAM_BOOL, 'Show in activity chooser'), ]); } /** * Toggles showinactivitychooser setting. * * @param int $tooltypeid the id of the course external tool type. * @param int $courseid the id of the course we are in. * @param bool $showinactivitychooser Show in activity chooser setting. * @return bool true or false */ public static function execute(int $tooltypeid, int $courseid, bool $showinactivitychooser): bool { [ 'tooltypeid' => $tooltypeid, 'courseid' => $courseid, 'showinactivitychooser' => $showinactivitychooser, ] = self::validate_parameters(self::execute_parameters(), [ 'tooltypeid' => $tooltypeid, 'courseid' => $courseid, 'showinactivitychooser' => $showinactivitychooser, ]); $context = \core\context\course::instance($courseid); self::validate_context($context); return types_helper::override_type_showinactivitychooser($tooltypeid, $courseid, $context, $showinactivitychooser); } /** * Get service returns definition. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_BOOL, 'Success'); } } classes/external/get_tool_types_and_proxies_count.php000064400000006023152265520610017404 0ustar00. namespace mod_lti\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * External function for fetching the count of all tool types and proxies. * * @package mod_lti * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_tool_types_and_proxies_count extends external_api { /** * Get parameter definition for get_tool_types_and_proxies_count(). * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'toolproxyid' => new external_value(PARAM_INT, 'Tool proxy id', VALUE_DEFAULT, 0), 'orphanedonly' => new external_value(PARAM_BOOL, 'Orphaned tool types only', VALUE_DEFAULT, 0), ] ); } /** * Get count of every tool type and tool proxy. * * @param int $toolproxyid The tool proxy id * @param bool $orphanedonly Whether to get orphaned proxies only. * @return array */ public static function execute($toolproxyid, $orphanedonly): array { $params = self::validate_parameters(self::execute_parameters(), [ 'toolproxyid' => $toolproxyid, 'orphanedonly' => $orphanedonly, ]); $toolproxyid = $params['toolproxyid']; $orphanedonly = $params['orphanedonly']; $context = \context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); return [ 'count' => lti_get_lti_types_and_proxies_count($orphanedonly, $toolproxyid), ]; } /** * Get return definition for get_tool_types_and_proxies_count. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'count' => new external_value(PARAM_INT, 'Total number of tool types and proxies', VALUE_REQUIRED), ]); } } classes/local/ltiopenid/registration_exception.php000064400000002174152265520610016573 0ustar00. /** * This library exposes functions for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; /** * Exception when transforming the registration to LTI config. * * Code is the HTTP Error code. */ class registration_exception extends \Exception { } classes/local/ltiopenid/registration_helper.php000064400000045560152265520610016062 0ustar00. /** * A Helper for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/mod/lti/locallib.php'); use Firebase\JWT\JWK; use Firebase\JWT\JWT; use Firebase\JWT\Key; use stdClass; /** * This class exposes functions for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class registration_helper { /** score scope */ const SCOPE_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** result scope */ const SCOPE_RESULT = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** lineitem read-only scope */ const SCOPE_LINEITEM_RO = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; /** lineitem full access scope */ const SCOPE_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; /** Names and Roles (membership) scope */ const SCOPE_NRPS = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; /** Tool Settings scope */ const SCOPE_TOOL_SETTING = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting'; /** Indicates the token is to create a new registration */ const REG_TOKEN_OP_NEW_REG = 'reg'; /** Indicates the token is to update an existing registration */ const REG_TOKEN_OP_UPDATE_REG = 'reg-update'; /** * Get an instance of this helper * * @return object */ public static function get() { return new registration_helper(); } /** * Function used to validate parameters. * * This function is needed because the payload contains nested * objects, and optional_param() does not support arrays of arrays. * * @param array $payload that may contain the parameter key * @param string $key the key of the value to be looked for in the payload * @param bool $required if required, not finding a value will raise a registration_exception * * @return mixed */ private function get_parameter(array $payload, string $key, bool $required) { if (!isset($payload[$key]) || empty($payload[$key])) { if ($required) { throw new registration_exception('missing required attribute '.$key, 400); } return null; } $parameter = $payload[$key]; // Cleans parameters to avoid XSS and other issues. if (is_array($parameter)) { return clean_param_array($parameter, PARAM_TEXT, true); } return clean_param($parameter, PARAM_TEXT); } /** * Transforms an LTI 1.3 Registration to a Moodle LTI Config. * * @param array $registrationpayload the registration data received from the tool. * @param string $clientid the clientid to be issued for that tool. * * @return object the Moodle LTI config. */ public function registration_to_config(array $registrationpayload, string $clientid): object { $responsetypes = $this->get_parameter($registrationpayload, 'response_types', true); $initiateloginuri = $this->get_parameter($registrationpayload, 'initiate_login_uri', true); $redirecturis = $this->get_parameter($registrationpayload, 'redirect_uris', true); $clientname = $this->get_parameter($registrationpayload, 'client_name', true); $jwksuri = $this->get_parameter($registrationpayload, 'jwks_uri', true); $tokenendpointauthmethod = $this->get_parameter($registrationpayload, 'token_endpoint_auth_method', true); $applicationtype = $this->get_parameter($registrationpayload, 'application_type', false); $logouri = $this->get_parameter($registrationpayload, 'logo_uri', false); $ltitoolconfiguration = $this->get_parameter($registrationpayload, 'https://purl.imsglobal.org/spec/lti-tool-configuration', true); $domain = $this->get_parameter($ltitoolconfiguration, 'domain', false); $targetlinkuri = $this->get_parameter($ltitoolconfiguration, 'target_link_uri', false); $customparameters = $this->get_parameter($ltitoolconfiguration, 'custom_parameters', false); $scopes = explode(" ", $this->get_parameter($registrationpayload, 'scope', false) ?? ''); $claims = $this->get_parameter($ltitoolconfiguration, 'claims', false); $messages = $ltitoolconfiguration['messages'] ?? []; $description = $this->get_parameter($ltitoolconfiguration, 'description', false); // Validate domain and target link. if (empty($domain)) { throw new registration_exception('missing_domain', 400); } $targetlinkuri = $targetlinkuri ?: 'https://'.$domain; // Stripping www as this is ignored for domain matching. $domain = lti_get_domain_from_url($domain); if ($domain !== lti_get_domain_from_url($targetlinkuri)) { throw new registration_exception('domain_targetlinkuri_mismatch', 400); } // Validate response type. // According to specification, for this scenario, id_token must be explicitly set. if (!in_array('id_token', $responsetypes)) { throw new registration_exception('invalid_response_types', 400); } // According to specification, this parameter needs to be an array. if (!is_array($redirecturis)) { throw new registration_exception('invalid_redirect_uris', 400); } // According to specification, for this scenario private_key_jwt must be explicitly set. if ($tokenendpointauthmethod !== 'private_key_jwt') { throw new registration_exception('invalid_token_endpoint_auth_method', 400); } if (!empty($applicationtype) && $applicationtype !== 'web') { throw new registration_exception('invalid_application_type', 400); } $config = new stdClass(); $config->lti_clientid = $clientid; $config->lti_toolurl = $targetlinkuri; $config->lti_tooldomain = $domain; $config->lti_typename = $clientname; $config->lti_description = $description; $config->lti_ltiversion = LTI_VERSION_1P3; $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID; $config->lti_icon = $logouri; $config->lti_coursevisible = LTI_COURSEVISIBLE_PRECONFIGURED; $config->lti_contentitem = 0; // Sets Content Item. if (!empty($messages)) { $messagesresponse = []; foreach ($messages as $value) { if ($value['type'] === 'LtiDeepLinkingRequest') { $config->lti_contentitem = 1; $config->lti_toolurl_ContentItemSelectionRequest = $value['target_link_uri'] ?? ''; array_push($messagesresponse, $value); } } } $config->lti_keytype = 'JWK_KEYSET'; $config->lti_publickeyset = $jwksuri; $config->lti_initiatelogin = $initiateloginuri; $config->lti_redirectionuris = implode(PHP_EOL, $redirecturis); $config->lti_customparameters = ''; // Sets custom parameters. if (isset($customparameters)) { $paramssarray = []; foreach ($customparameters as $key => $value) { array_push($paramssarray, $key . '=' . $value); } $config->lti_customparameters = implode(PHP_EOL, $paramssarray); } // Sets launch container. $config->lti_launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; // Sets Service info based on scopes. $config->lti_acceptgrades = LTI_SETTING_NEVER; $config->ltiservice_gradesynchronization = 0; $config->ltiservice_memberships = 0; $config->ltiservice_toolsettings = 0; if (isset($scopes)) { // Sets Assignment and Grade Services info. if (in_array(self::SCOPE_SCORE, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_RESULT, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_LINEITEM_RO, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_LINEITEM, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 2; } // Sets Names and Role Provisioning info. if (in_array(self::SCOPE_NRPS, $scopes)) { $config->ltiservice_memberships = 1; } // Sets Tool Settings info. if (in_array(self::SCOPE_TOOL_SETTING, $scopes)) { $config->ltiservice_toolsettings = 1; } } // Sets privacy settings. $config->lti_sendname = LTI_SETTING_NEVER; $config->lti_sendemailaddr = LTI_SETTING_NEVER; if (isset($claims)) { // Sets name privacy settings. if (in_array('name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } if (in_array('given_name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } if (in_array('family_name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } // Sets email privacy settings. if (in_array('email', $claims)) { $config->lti_sendemailaddr = LTI_SETTING_ALWAYS; } } return $config; } /** * Adds to the config the LTI 1.1 key and sign it with the 1.1 secret. * * @param array $lticonfig reference to lticonfig to which to add the 1.1 OAuth info. * @param string $key - LTI 1.1 OAuth Key * @param string $secret - LTI 1.1 OAuth Secret * */ private function add_previous_key_claim(array &$lticonfig, string $key, string $secret) { if ($key) { $oauthconsumer = []; $oauthconsumer['key'] = $key; $oauthconsumer['nonce'] = random_string(random_int(10, 20)); $oauthconsumer['sign'] = hash('sha256', $key.$secret.$oauthconsumer['nonce']); $lticonfig['oauth_consumer'] = $oauthconsumer; } } /** * Transforms a moodle LTI 1.3 Config to an OAuth/LTI Client Registration. * * @param object $config Moodle LTI Config. * @param int $typeid which is the LTI deployment id. * @param object $type tool instance in case the tool already exists. * * @return array the Client Registration as an associative array. */ public function config_to_registration(object $config, int $typeid, ?object $type = null): array { $configarray = []; foreach ((array)$config as $k => $v) { if (substr($k, 0, 4) == 'lti_') { $k = substr($k, 4); } $configarray[$k] = $v; } $config = (object) $configarray; $registrationresponse = []; $lticonfigurationresponse = []; $ltiversion = $type ? $type->ltiversion : $config->ltiversion; $lticonfigurationresponse['version'] = $ltiversion; if ($ltiversion === LTI_VERSION_1P3) { $registrationresponse['client_id'] = $type ? $type->clientid : $config->clientid; $registrationresponse['response_types'] = ['id_token']; $registrationresponse['jwks_uri'] = $config->publickeyset; $registrationresponse['initiate_login_uri'] = $config->initiatelogin; $registrationresponse['grant_types'] = ['client_credentials', 'implicit']; $registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->redirectionuris); $registrationresponse['application_type'] = 'web'; $registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt'; } else if ($ltiversion === LTI_VERSION_1 && $type) { $this->add_previous_key_claim($lticonfigurationresponse, $config->resourcekey, $config->password); } else if ($ltiversion === LTI_VERSION_2 && $type) { $toolproxy = $this->get_tool_proxy($type->toolproxyid); $this->add_previous_key_claim($lticonfigurationresponse, $toolproxy['guid'], $toolproxy['secret']); } $registrationresponse['client_name'] = $type ? $type->name : $config->typename; $registrationresponse['logo_uri'] = $type ? ($type->secureicon ?? $type->icon ?? '') : $config->icon ?? ''; $lticonfigurationresponse['deployment_id'] = strval($typeid); $lticonfigurationresponse['target_link_uri'] = $type ? $type->baseurl : $config->toolurl ?? ''; $lticonfigurationresponse['domain'] = $type ? $type->tooldomain : $config->tooldomain ?? ''; $lticonfigurationresponse['description'] = $type ? $type->description ?? '' : $config->description ?? ''; if ($config->contentitem ?? 0 == 1) { $contentitemmessage = []; $contentitemmessage['type'] = 'LtiDeepLinkingRequest'; if (isset($config->toolurl_ContentItemSelectionRequest)) { $contentitemmessage['target_link_uri'] = $config->toolurl_ContentItemSelectionRequest; } $lticonfigurationresponse['messages'] = [$contentitemmessage]; } if (isset($config->customparameters) && !empty($config->customparameters)) { $params = []; foreach (explode(PHP_EOL, $config->customparameters) as $param) { $split = explode('=', $param); $params[$split[0]] = $split[1]; } $lticonfigurationresponse['custom_parameters'] = $params; } $scopesresponse = []; if ($config->ltiservice_gradesynchronization ?? 0 > 0) { $scopesresponse[] = self::SCOPE_SCORE; $scopesresponse[] = self::SCOPE_RESULT; $scopesresponse[] = self::SCOPE_LINEITEM_RO; } if ($config->ltiservice_gradesynchronization ?? 0 == 2) { $scopesresponse[] = self::SCOPE_LINEITEM; } if ($config->ltiservice_memberships ?? 0 == 1) { $scopesresponse[] = self::SCOPE_NRPS; } if ($config->ltiservice_toolsettings ?? 0 == 1) { $scopesresponse[] = self::SCOPE_TOOL_SETTING; } $registrationresponse['scope'] = implode(' ', $scopesresponse); $claimsresponse = ['sub', 'iss']; if ($config->sendname ?? '' == LTI_SETTING_ALWAYS) { $claimsresponse[] = 'name'; $claimsresponse[] = 'family_name'; $claimsresponse[] = 'given_name'; } if ($config->sendemailaddr ?? '' == LTI_SETTING_ALWAYS) { $claimsresponse[] = 'email'; } $lticonfigurationresponse['claims'] = $claimsresponse; $registrationresponse['https://purl.imsglobal.org/spec/lti-tool-configuration'] = $lticonfigurationresponse; return $registrationresponse; } /** * Validates the registration token is properly signed and not used yet. * Return the client id to use for this registration. * * @param string $registrationtokenjwt registration token * * @return array with 2 keys: clientid for the registration, type but only if it's an update */ public function validate_registration_token(string $registrationtokenjwt): array { global $DB; // JWK::parseKeySet uses RS256 algorithm by default. $keys = JWK::parseKeySet(jwks_helper::get_jwks()); $registrationtoken = JWT::decode($registrationtokenjwt, $keys); $response = []; // Get clientid from registrationtoken. $clientid = $registrationtoken->sub; if ($registrationtoken->scope == self::REG_TOKEN_OP_NEW_REG) { // Checks if clientid is already registered. if (!empty($DB->get_record('lti_types', array('clientid' => $clientid)))) { throw new registration_exception("token_already_used", 401); } $response['clientid'] = $clientid; } else if ($registrationtoken->scope == self::REG_TOKEN_OP_UPDATE_REG) { $tool = lti_get_type($registrationtoken->sub); if (!$tool) { throw new registration_exception("Unknown client", 400); } $response['clientid'] = $tool->clientid ?? $this->new_clientid(); $response['type'] = $tool; } else { throw new registration_exception("Incorrect scope", 403); } return $response; } /** * Initializes an array with the scopes for services supported by the LTI module * * @return array List of scopes */ public function lti_get_service_scopes() { $services = lti_get_services(); $scopes = array(); foreach ($services as $service) { $servicescopes = $service->get_scopes(); if (!empty($servicescopes)) { $scopes = array_merge($scopes, $servicescopes); } } return $scopes; } /** * Generates a new client id string. * * @return string generated client id */ public function new_clientid(): string { return random_string(15); } /** * Base64 encoded signature for LTI 1.1 migration. * @param string $key LTI 1.1 key * @param string $salt Salt value * @param string $secret LTI 1.1 secret * * @return string base64encoded hash */ public function sign(string $key, string $salt, string $secret): string { return base64_encode(hash_hmac('sha-256', $key.$salt, $secret, true)); } /** * Returns a tool proxy * * @param int $proxyid * * @return mixed Tool Proxy details */ public function get_tool_proxy(int $proxyid): array { return lti_get_tool_proxy($proxyid); } } classes/local/ltiopenid/jwks_helper.php000064400000011260152265520610014314 0ustar00. /** * This files exposes functions for LTI 1.3 Key Management. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; use Firebase\JWT\JWT; /** * This class exposes functions for LTI 1.3 Key Management. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class jwks_helper { /** * * See https://www.imsglobal.org/spec/security/v1p1#approved-jwt-signing-algorithms. * @var string[] */ private static $ltisupportedalgs = [ 'RS256' => 'RSA', 'RS384' => 'RSA', 'RS512' => 'RSA', 'ES256' => 'EC', 'ES384' => 'EC', 'ES512' => 'EC' ]; /** * Returns the private key to use to sign outgoing JWT. * * @return array keys are kid and key in PEM format. */ public static function get_private_key() { $privatekey = get_config('mod_lti', 'privatekey'); $kid = get_config('mod_lti', 'kid'); return [ "key" => $privatekey, "kid" => $kid ]; } /** * Returns the JWK Key Set for this site. * @return array keyset exposting the site public key. */ public static function get_jwks() { $jwks = array('keys' => array()); $privatekey = self::get_private_key(); $res = openssl_pkey_get_private($privatekey['key']); $details = openssl_pkey_get_details($res); // Avoid passing null values to base64_encode. if (!isset($details['rsa']['e']) || !isset($details['rsa']['n'])) { throw new \moodle_exception('Error: essential openssl keys not set'); } $jwk = array(); $jwk['kty'] = 'RSA'; $jwk['alg'] = 'RS256'; $jwk['kid'] = $privatekey['kid']; $jwk['e'] = rtrim(strtr(base64_encode($details['rsa']['e']), '+/', '-_'), '='); $jwk['n'] = rtrim(strtr(base64_encode($details['rsa']['n']), '+/', '-_'), '='); $jwk['use'] = 'sig'; $jwks['keys'][] = $jwk; return $jwks; } /** * Take an array of JWKS keys and infer the 'alg' property for a single key, if missing, based on an input JWT. * * This only sets the 'alg' property for a single key when all the following conditions are met: * - The key's 'kid' matches the 'kid' provided in the JWT's header. * - The key's 'alg' is missing. * - The JWT's header 'alg' matches the algorithm family of the key (the key's kty). * - The JWT's header 'alg' matches one of the approved LTI asymmetric algorithms. * * Keys not matching the above are left unchanged. * * @param array $jwks the keyset array. * @param string $jwt the JWT string. * @return array the fixed keyset array. */ public static function fix_jwks_alg(array $jwks, string $jwt): array { $jwtparts = explode('.', $jwt); $jwtheader = json_decode(JWT::urlsafeB64Decode($jwtparts[0]), true); if (!isset($jwtheader['kid'])) { throw new \moodle_exception('Error: kid must be provided in JWT header.'); } foreach ($jwks['keys'] as $index => $key) { // Only fix the key being referred to in the JWT. if ($jwtheader['kid'] != $key['kid']) { continue; } // Only fix the key if the alg is missing. if (!empty($key['alg'])) { continue; } // The header alg must match the key type (family) specified in the JWK's kty. if (!isset(static::$ltisupportedalgs[$jwtheader['alg']]) || static::$ltisupportedalgs[$jwtheader['alg']] != $key['kty']) { throw new \moodle_exception('Error: Alg specified in the JWT header is incompatible with the JWK key type'); } $jwks['keys'][$index]['alg'] = $jwtheader['alg']; } return $jwks; } } classes/local/ltiservice/response.php000064400000015656152265520610014034 0ustar00. /** * This file contains an abstract definition of an LTI service * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die; /** * The mod_lti\local\ltiservice\response class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class response { /** @var int HTTP response code. */ private $code; /** @var string HTTP response reason. */ private $reason; /** @var string HTTP request method. */ private $requestmethod; /** @var string HTTP request accept header. */ private $accept; /** @var string HTTP response content type. */ private $contenttype; /** @var string HTTP request body. */ private $data; /** @var string HTTP response body. */ private $body; /** @var array HTTP response codes. */ private $responsecodes; /** @var array HTTP additional headers. */ private $additionalheaders; /** * Class constructor. */ public function __construct() { $this->code = 200; $this->reason = ''; $this->requestmethod = $_SERVER['REQUEST_METHOD']; $this->accept = ''; $this->contenttype = ''; $this->data = ''; $this->body = ''; $this->responsecodes = array( 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 300 => 'Multiple Choices', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 415 => 'Unsupported Media Type', 500 => 'Internal Server Error', 501 => 'Not Implemented' ); $this->additionalheaders = array(); } /** * Get the response code. * * @return int */ public function get_code() { return $this->code; } /** * Set the response code. * * @param int $code Response code */ public function set_code($code) { $this->code = $code; $this->reason = ''; } /** * Get the response reason. * * @return string */ public function get_reason() { $code = $this->code; if (($code < 200) || ($code >= 600)) { $code = 500; // Status code must be between 200 and 599. } if (empty($this->reason) && array_key_exists($code, $this->responsecodes)) { $this->reason = $this->responsecodes[$code]; } // Use generic reason for this category (based on first digit) if a specific reason is not defined. if (empty($this->reason)) { $this->reason = $this->responsecodes[intval($code / 100) * 100]; } return $this->reason; } /** * Set the response reason. * * @param string $reason Reason */ public function set_reason($reason) { $this->reason = $reason; } /** * Get the request method. * * @return string */ public function get_request_method() { return $this->requestmethod; } /** * Get the request accept header. * * @return string */ public function get_accept() { return $this->accept; } /** * Set the request accept header. * * @param string $accept Accept header value */ public function set_accept($accept) { $this->accept = $accept; } /** * Get the response content type. * * @return string */ public function get_content_type() { return $this->contenttype; } /** * Set the response content type. * * @param string $contenttype Content type */ public function set_content_type($contenttype) { $this->contenttype = $contenttype; } /** * Get the request body. * * @return string */ public function get_request_data() { return $this->data; } /** * Set the response body. * * @param string $data Body data */ public function set_request_data($data) { $this->data = $data; } /** * Get the response body. * * @return string */ public function get_body() { return $this->body; } /** * Set the response body. * * @param string $body Body data */ public function set_body($body) { $this->body = $body; } /** * Add an additional header. * * @param string $header The new header */ public function add_additional_header($header) { array_push($this->additionalheaders, $header); } /** * Send the response. */ public function send() { header("HTTP/1.0 {$this->code} {$this->get_reason()}"); foreach ($this->additionalheaders as $header) { header($header); } if ((($this->code >= 200) && ($this->code < 300)) || !empty($this->body)) { if (!empty($this->contenttype)) { header("Content-Type: {$this->contenttype}; charset=utf-8"); } if (!empty($this->body)) { echo $this->body; } } else if ($this->code >= 400) { header("Content-Type: application/json; charset=utf-8"); $body = new \stdClass(); $body->status = $this->code; $body->reason = $this->get_reason(); $body->request = new \stdClass(); $body->request->method = $_SERVER['REQUEST_METHOD']; $body->request->url = $_SERVER['REQUEST_URI']; if (isset($_SERVER['HTTP_ACCEPT'])) { $body->request->accept = $_SERVER['HTTP_ACCEPT']; } if (isset($_SERVER['CONTENT_TYPE'])) { $body->request->contentType = explode(';', $_SERVER['CONTENT_TYPE'], 2)[0]; } echo json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } } } classes/local/ltiservice/resource_base.php000064400000027464152265520610015017 0ustar00. /** * This file contains an abstract definition of an LTI resource * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * The mod_lti\local\ltiservice\resource_base class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class resource_base { /** HTTP Post method */ const HTTP_POST = 'POST'; /** HTTP Get method */ const HTTP_GET = 'GET'; /** HTTP Put method */ const HTTP_PUT = 'PUT'; /** HTTP Delete method */ const HTTP_DELETE = 'DELETE'; /** @var service_base Service associated with this resource. */ private $service; /** @var string Type for this resource. */ protected $type; /** @var string ID for this resource. */ protected $id; /** @var string Template for this resource. */ protected $template; /** @var array Custom parameter substitution variables associated with this resource. */ protected $variables; /** @var array Media types supported by this resource. */ protected $formats; /** @var array HTTP actions supported by this resource. */ protected $methods; /** @var array Template variables parsed from the resource template. */ protected $params; /** * Class constructor. * * @param service_base $service Service instance */ public function __construct($service) { $this->service = $service; $this->type = 'RestService'; $this->id = null; $this->template = null; $this->methods = array(); $this->variables = array(); $this->formats = array(); $this->methods = array(); $this->params = null; } /** * Get the resource ID. * * @return string */ public function get_id() { return $this->id; } /** * Get the resource template. * * @return string */ public function get_template() { return $this->template; } /** * Get the resource path. * * @return string */ public function get_path() { return $this->get_template(); } /** * Get the resource type. * * @return string */ public function get_type() { return $this->type; } /** * Get the resource's service. * * @return service_base */ public function get_service() { return $this->service; } /** * Get the resource methods. * * @return array */ public function get_methods() { return $this->methods; } /** * Get the resource media types. * * @return array */ public function get_formats() { return $this->formats; } /** * Get the resource template variables. * * @return array */ public function get_variables() { return $this->variables; } /** * Get the resource fully qualified endpoint. * * @return string */ public function get_endpoint() { $this->parse_template(); $template = preg_replace('/[\(\)]/', '', $this->get_template()); $url = $this->get_service()->get_service_path() . $template; foreach ($this->params as $key => $value) { $url = str_replace('{' . $key . '}', $value, $url); } $toolproxy = $this->get_service()->get_tool_proxy(); if (!empty($toolproxy)) { $url = str_replace('{config_type}', 'toolproxy', $url); $url = str_replace('{tool_proxy_id}', $toolproxy->guid, $url); } else { $url = str_replace('{config_type}', 'tool', $url); $url = str_replace('{tool_proxy_id}', $this->get_service()->get_type()->id, $url); } return $url; } /** * Execute the request for this resource. * * @param response $response Response object for this request. */ abstract public function execute($response); /** * Check to make sure the request is valid. * * @param int $typeid The typeid we want to use * @param string $body Body of HTTP request message * @param string[] $scopes Array of scope(s) required for incoming request * * @return boolean */ public function check_tool($typeid, $body = null, $scopes = null) { $ok = $this->get_service()->check_tool($typeid, $body, $scopes); if ($ok) { if ($this->get_service()->get_tool_proxy()) { $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy; } if (!empty($toolproxyjson)) { // Check tool proxy to ensure service being requested is included. $toolproxy = json_decode($toolproxyjson); if (!empty($toolproxy) && isset($toolproxy->security_contract->tool_service)) { $contexts = lti_get_contexts($toolproxy); $tpservices = $toolproxy->security_contract->tool_service; foreach ($tpservices as $service) { $fqid = lti_get_fqid($contexts, $service->service); $id = explode('#', $fqid, 2); if ($this->get_id() === $id[1]) { $ok = true; break; } } } if (!$ok) { debugging('Requested service not permitted: ' . $this->get_id(), DEBUG_DEVELOPER); } } else { // Check that the scope required for the service request is included in those granted for the // access token being used. $permittedscopes = $this->get_service()->get_permitted_scopes(); $ok = is_null($permittedscopes) || empty($scopes) || !empty(array_intersect($permittedscopes, $scopes)); } } return $ok; } /** * Check to make sure the request is valid. * * @param string $toolproxyguid Consumer key * @param string $body Body of HTTP request message * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function check_tool_proxy($toolproxyguid, $body = null) { debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; if ($this->get_service()->check_tool_proxy($toolproxyguid, $body)) { $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy; if (empty($toolproxyjson)) { $ok = true; } else { $toolproxy = json_decode($toolproxyjson); if (!empty($toolproxy) && isset($toolproxy->security_contract->tool_service)) { $contexts = lti_get_contexts($toolproxy); $tpservices = $toolproxy->security_contract->tool_service; foreach ($tpservices as $service) { $fqid = lti_get_fqid($contexts, $service->service); $id = explode('#', $fqid, 2); if ($this->get_id() === $id[1]) { $ok = true; break; } } } if (!$ok) { debugging('Requested service not included in tool proxy: ' . $this->get_id()); } } } return $ok; } /** * Check to make sure the request is valid. * * @param int $typeid The typeid we want to use * @param int $contextid The course we are at * @param string $permissionrequested The permission to be checked * @param string $body Body of HTTP request message * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function check_type($typeid, $contextid, $permissionrequested, $body = null) { debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; if ($this->get_service()->check_type($typeid, $contextid, $body)) { $neededpermissions = $this->get_permissions($typeid); foreach ($neededpermissions as $permission) { if ($permission == $permissionrequested) { $ok = true; break; } } if (!$ok) { debugging('Requested service ' . $permissionrequested . ' not included in tool type: ' . $typeid, DEBUG_DEVELOPER); } } return $ok; } /** * get permissions from the config of the tool for that resource * * @param int $ltitype Type of LTI * @return array with the permissions related to this resource by the $ltitype or empty if none. * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function get_permissions($ltitype) { debugging('get_permissions() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); return array(); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { return $value; } /** * Parse the template for variables. * * @return array */ protected function parse_template() { if (empty($this->params)) { $this->params = array(); if (!empty($_SERVER['PATH_INFO'])) { $path = explode('/', $_SERVER['PATH_INFO']); $template = preg_replace('/\([0-9a-zA-Z_\-,\/]+\)/', '', $this->get_template()); $parts = explode('/', $template); for ($i = 0; $i < count($parts); $i++) { if ((substr($parts[$i], 0, 1) == '{') && (substr($parts[$i], -1) == '}')) { $value = ''; if ($i < count($path)) { $value = $path[$i]; } $this->params[substr($parts[$i], 1, -1)] = $value; } } } } return $this->params; } } classes/local/ltiservice/service_base.php000064400000036735152265520610014631 0ustar00. /** * This file contains an abstract definition of an LTI service * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/OAuthBody.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; use stdClass; /** * The mod_lti\local\ltiservice\service_base class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class service_base { /** Label representing an LTI 2 message type */ const LTI_VERSION2P0 = 'LTI-2p0'; /** Service enabled */ const SERVICE_ENABLED = 1; /** @var string ID for the service. */ protected $id; /** @var string Human readable name for the service. */ protected $name; /** @var boolean true if requests for this service do not need to be signed. */ protected $unsigned; /** @var stdClass Tool proxy object for the current service request. */ private $toolproxy; /** @var stdClass LTI type object for the current service request. */ private $type; /** @var array LTI type config array for the current service request. */ private $typeconfig; /** @var array Instances of the resources associated with this service. */ protected $resources; /** * Class constructor. */ public function __construct() { $this->id = null; $this->name = null; $this->unsigned = false; $this->toolproxy = null; $this->type = null; $this->typeconfig = null; $this->resources = null; } /** * Get the service ID. * * @return string */ public function get_id() { return $this->id; } /** * Get the service compoent ID. * * @return string */ public function get_component_id() { return 'ltiservice_' . $this->id; } /** * Get the service name. * * @return string */ public function get_name() { return $this->name; } /** * Get whether the service requests need to be signed. * * @return boolean */ public function is_unsigned() { return $this->unsigned; } /** * Get the tool proxy object. * * @return stdClass */ public function get_tool_proxy() { return $this->toolproxy; } /** * Set the tool proxy object. * * @param object $toolproxy The tool proxy for this service request * * @var stdClass */ public function set_tool_proxy($toolproxy) { $this->toolproxy = $toolproxy; } /** * Get the type object. * * @return stdClass */ public function get_type() { return $this->type; } /** * Set the LTI type object. * * @param object $type The LTI type for this service request * * @var stdClass */ public function set_type($type) { $this->type = $type; } /** * Get the type config array. * * @return array|null */ public function get_typeconfig() { return $this->typeconfig; } /** * Set the LTI type config object. * * @param array $typeconfig The LTI type config for this service request * * @var array */ public function set_typeconfig($typeconfig) { $this->typeconfig = $typeconfig; } /** * Get the resources for this service. * * @return resource_base[] */ abstract public function get_resources(); /** * Get the scope(s) permitted for this service in the context of a particular tool type. * * A null value indicates that no scopes are required to access the service. * * @return array|null */ public function get_permitted_scopes() { return null; } /** * Get the scope(s) permitted for this service. * * A null value indicates that no scopes are required to access the service. * * @return array|null */ public function get_scopes() { return null; } /** * Returns the configuration options for this service. * * @param \MoodleQuickForm $mform Moodle quickform object definition */ public function get_configuration_options(&$mform) { } /** * Called when a new LTI Instance is added. * * @param object $lti LTI Instance. */ public function instance_added(object $lti): void { } /** * Called when a new LTI Instance is updated. * * @param object $lti LTI Instance. */ public function instance_updated(object $lti): void { } /** * Called when the launch data is created, offering a possibility to alter the * target link URI. * * @param string $messagetype message type for this launch * @param string $targetlinkuri current target link uri * @param null|string $customstr concatenated list of custom parameters * @param int $courseid * @param null|object $lti LTI Instance. * * @return array containing the target link URL and the custom params string to use. */ public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid, ?object $lti = null): array { return [$targetlinkuri, $customstr]; } /** * Called when a new LTI Instance is deleted. * * @param int $id LTI Instance. */ public function instance_deleted(int $id): void { } /** * Set the form data when displaying the LTI Instance form. * * @param object $defaultvalues Default form values. */ public function set_instance_form_values(object $defaultvalues): void { } /** * Return an array with the names of the parameters that the service will be saving in the configuration * * @return array Names list of the parameters that the service will be saving in the configuration * @deprecated since Moodle 3.7 - please do not use this function any more. */ public function get_configuration_parameter_names() { debugging('get_configuration_parameter_names() has been deprecated.', DEBUG_DEVELOPER); return array(); } /** * Default implementation will check for the existence of at least one mod_lti entry for that tool and context. * * It may be overridden if other inferences can be done. * * Ideally a Site Tool should be explicitly engaged with a course, the check on the presence of a link is a proxy * to infer a Site Tool engagement until an explicit Site Tool - Course relationship exists. * * @param int $typeid The tool lti type id. * @param int $courseid The course id. * @return bool returns True if tool is used in context, false otherwise. */ public function is_used_in_context($typeid, $courseid) { global $DB; $ok = $DB->record_exists('lti', array('course' => $courseid, 'typeid' => $typeid)); return $ok || $DB->record_exists('lti_types', array('course' => $courseid, 'id' => $typeid)); } /** * Checks if there is a site tool or a course tool for this site. * * @param int $typeid The tool lti type id. * @param int $courseid The course id. * @return bool returns True if tool is allowed in context, false otherwise. */ public function is_allowed_in_context($typeid, $courseid) { global $DB; // Check if it is a Course tool for this course or a Site tool. $type = $DB->get_record('lti_types', array('id' => $typeid)); return $type && ($type->course == $courseid || $type->course == SITEID); } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid The course id. * @param string $userid The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration and not return parameters for services not used. * * @return array Key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $userid, $typeid, $modlti = null) { return array(); } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return []; } /** * Get the path for service requests. * * @return string */ public static function get_service_path() { $url = new \moodle_url('/mod/lti/services.php'); return $url->out(false); } /** * Parse a string for custom substitution parameter variables supported by this service's resources. * * @param string $value Value to be parsed * * @return string */ public function parse_value($value) { if (empty($this->resources)) { $this->resources = $this->get_resources(); } if (!empty($this->resources)) { foreach ($this->resources as $resource) { $value = $resource->parse_value($value); } } return $value; } /** * Check that the request has been properly signed and is permitted. * * @param string $typeid LTI type ID * @param string $body Request body (null if none) * @param string[] $scopes Array of required scope(s) for incoming request * * @return boolean */ public function check_tool($typeid, $body = null, $scopes = null) { $ok = true; $toolproxy = null; $consumerkey = lti\get_oauth_key_from_headers($typeid, $scopes); if ($consumerkey === false) { $ok = $this->is_unsigned(); } else { if (empty($typeid) && is_int($consumerkey)) { $typeid = $consumerkey; } if (!empty($typeid)) { $this->type = lti_get_type($typeid); $this->typeconfig = lti_get_type_config($typeid); $ok = !empty($this->type->id); if ($ok && !empty($this->type->toolproxyid)) { $this->toolproxy = lti_get_tool_proxy($this->type->toolproxyid); } } else { $toolproxy = lti_get_tool_proxy_from_guid($consumerkey); if ($toolproxy !== false) { $this->toolproxy = $toolproxy; } } } if ($ok && is_string($consumerkey)) { if (!empty($this->toolproxy)) { $key = $this->toolproxy->guid; $secret = $this->toolproxy->secret; } else { $key = $this->typeconfig['resourcekey']; $secret = $this->typeconfig['password']; } if (!$this->is_unsigned() && ($key == $consumerkey)) { $ok = $this->check_signature($key, $secret, $body); } else { $ok = $this->is_unsigned(); } } return $ok; } /** * Check that the request has been properly signed. * * @param string $toolproxyguid Tool Proxy GUID * @param string $body Request body (null if none) * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see service_base::check_tool() */ public function check_tool_proxy($toolproxyguid, $body = null) { debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' . 'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; $toolproxy = null; $consumerkey = lti\get_oauth_key_from_headers(); if (empty($toolproxyguid)) { $toolproxyguid = $consumerkey; } if (!empty($toolproxyguid)) { $toolproxy = lti_get_tool_proxy_from_guid($toolproxyguid); if ($toolproxy !== false) { if (!$this->is_unsigned() && ($toolproxy->guid == $consumerkey)) { $ok = $this->check_signature($toolproxy->guid, $toolproxy->secret, $body); } else { $ok = $this->is_unsigned(); } } } if ($ok) { $this->toolproxy = $toolproxy; } return $ok; } /** * Check that the request has been properly signed. * * @param int $typeid The tool id * @param int $courseid The course we are at * @param string $body Request body (null if none) * * @return bool * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see service_base::check_tool() */ public function check_type($typeid, $courseid, $body = null) { debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' . 'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; $tool = null; $consumerkey = lti\get_oauth_key_from_headers(); if (empty($typeid)) { return $ok; } else if ($this->is_allowed_in_context($typeid, $courseid)) { $tool = lti_get_type_type_config($typeid); if ($tool !== false) { if (!$this->is_unsigned() && ($tool->lti_resourcekey == $consumerkey)) { $ok = $this->check_signature($tool->lti_resourcekey, $tool->lti_password, $body); } else { $ok = $this->is_unsigned(); } } } return $ok; } /** * Check the request signature. * * @param string $consumerkey Consumer key * @param string $secret Shared secret * @param string $body Request body * * @return boolean */ private function check_signature($consumerkey, $secret, $body) { $ok = true; try { // TODO: Switch to core oauthlib once implemented - MDL-30149. lti\handle_oauth_body_post($consumerkey, $secret, $body); } catch (\Exception $e) { debugging($e->getMessage() . "\n"); $ok = false; } return $ok; } }classes/local/types_helper.php000064400000015072152265520610012520 0ustar00. namespace mod_lti\local; use core\context\course; /** * Helper class specifically dealing with LTI types (preconfigured tools). * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class types_helper { /** * Returns all LTI tool types (preconfigured tools) visible in the given course and for the given user. * * This list will contain both site level tools and course-level tools. * * @param int $courseid the id of the course. * @param int $userid the id of the user. * @param array $coursevisible options for 'coursevisible' field, which will default to * [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER] if omitted. * @return \stdClass[] the array of tool type objects. */ public static function get_lti_types_by_course(int $courseid, int $userid, array $coursevisible = []): array { global $DB, $SITE; if (!has_capability('mod/lti:addpreconfiguredinstance', course::instance($courseid), $userid)) { return []; } if (empty($coursevisible)) { $coursevisible = [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER]; } [$coursevisiblesql, $coursevisparams] = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible'); [$coursevisiblesql1, $coursevisparams1] = $DB->get_in_or_equal($coursevisible, SQL_PARAMS_NAMED, 'coursevisible'); [$coursevisibleoverriddensql, $coursevisoverriddenparams] = $DB->get_in_or_equal( $coursevisible, SQL_PARAMS_NAMED, 'coursevisibleoverridden'); $coursecond = implode(" OR ", ["t.course = :courseid", "t.course = :siteid"]); $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]); $query = "SELECT * FROM (SELECT t.*, c.coursevisible as coursevisibleoverridden FROM {lti_types} t LEFT JOIN {lti_types_categories} tc ON t.id = tc.typeid LEFT JOIN {lti_coursevisible} c ON c.typeid = t.id AND c.courseid = $courseid WHERE (t.coursevisible $coursevisiblesql OR (c.coursevisible $coursevisiblesql1 AND t.coursevisible NOT IN (:lticoursevisibleno))) AND ($coursecond) AND t.state = :active AND (tc.id IS NULL OR tc.categoryid = :categoryid)) tt WHERE tt.coursevisibleoverridden IS NULL OR tt.coursevisibleoverridden $coursevisibleoverriddensql"; return $DB->get_records_sql( $query, [ 'siteid' => $SITE->id, 'courseid' => $courseid, 'active' => LTI_TOOL_STATE_CONFIGURED, 'categoryid' => $coursecategory, 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'lticoursevisibleno' => LTI_COURSEVISIBLE_NO, ] + $coursevisparams + $coursevisparams1 + $coursevisoverriddenparams ); } /** * Override coursevisible for a given tool on course level. * * @param int $tooltypeid Type ID * @param int $courseid Course ID * @param \core\context\course $context Course context * @param bool $showinactivitychooser Show or not show in activity chooser * @return bool True if the coursevisible was changed, false otherwise. */ public static function override_type_showinactivitychooser(int $tooltypeid, int $courseid, \core\context\course $context, bool $showinactivitychooser): bool { global $DB; require_capability('mod/lti:addcoursetool', $context); $ltitype = lti_get_type($tooltypeid); if ($ltitype && ($ltitype->coursevisible != LTI_COURSEVISIBLE_NO)) { $coursevisible = $showinactivitychooser ? LTI_COURSEVISIBLE_ACTIVITYCHOOSER : LTI_COURSEVISIBLE_PRECONFIGURED; $ltitype->coursevisible = $coursevisible; $config = new \stdClass(); $config->lti_coursevisible = $coursevisible; if (intval($ltitype->course) != intval(get_site()->id)) { // It is course tool - just update it. lti_update_type($ltitype, $config); } else { $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]); $sql = "SELECT COUNT(*) AS count FROM {lti_types_categories} tc WHERE tc.typeid = :typeid"; $restrictedtool = $DB->count_records_sql($sql, ['typeid' => $tooltypeid]); if ($restrictedtool) { $record = $DB->get_record('lti_types_categories', ['typeid' => $tooltypeid, 'categoryid' => $coursecategory]); if (!$record) { throw new \moodle_exception('You are not allowed to change this setting for this tool.'); } } // This is site tool, but we would like to have course level setting for it. $lticoursevisible = $DB->get_record('lti_coursevisible', ['typeid' => $tooltypeid, 'courseid' => $courseid]); if (!$lticoursevisible) { $lticoursevisible = new \stdClass(); $lticoursevisible->typeid = $tooltypeid; $lticoursevisible->courseid = $courseid; $lticoursevisible->coursevisible = $coursevisible; $DB->insert_record('lti_coursevisible', $lticoursevisible); } else { $lticoursevisible->coursevisible = $coursevisible; $DB->update_record('lti_coursevisible', $lticoursevisible); } } return true; } return false; } } classes/plugininfo/ltiservice.php000064400000003150152265520610013240 0ustar00. /** * LTI service plugin info. * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti\plugininfo\ltiservice class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ltiservice extends base { /** * Should there be a way to uninstall the plugin via the administration UI? * * Uninstallation is not allowed for core subplugins. * * @return boolean */ public function is_uninstall_allowed() { if ($this->is_standard()) { return false; } return true; } } classes/plugininfo/ltisource.php000064400000005423152265520610013105 0ustar00. /** * LTI source plugin info. * * @package mod_lti * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); class ltisource extends base { /** * Returns the node name used in admin settings menu for this plugin settings (if applicable) * * @return null|string node name or null if plugin does not create settings node (default) */ public function get_settings_section_name() { return 'ltisourcesetting'.$this->name; } /** * Loads plugin settings to the settings tree * * This function usually includes settings.php file in plugins folder. * Alternatively it can create a link to some settings page (instance of admin_externalpage) * * @param \part_of_admin_tree $adminroot * @param string $parentnodename * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them. $ADMIN = $adminroot; // May be used in settings.php. $plugininfo = $this; // Also can be used inside settings.php. if (!$this->is_installed_and_upgraded()) { return; } if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) { return; } $section = $this->get_settings_section_name(); $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); include($this->full_path('settings.php')); // This may also set $settings to null. if ($settings) { $ADMIN->add($parentnodename, $settings); } } /** * Should there be a way to uninstall the plugin via the administration UI. * * @return bool */ public function is_uninstall_allowed() { return true; } } classes/helper.php000064400000004475152265520610010207 0ustar00. namespace mod_lti; defined('MOODLE_INTERNAL') || die(); /** * Helper class for LTI activity. * * @package mod_lti * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get SQL to query DB for LTI tool proxy records. * * @param bool $orphanedonly If true, return SQL to get orphaned proxies only. * @param bool $count If true, return SQL to get the count of the records instead of the records themselves. * @return string SQL. */ public static function get_tool_proxy_sql(bool $orphanedonly = false, bool $count = false): string { if ($count) { $select = "SELECT count(*) as type_count"; $sort = ""; } else { // We only want the fields from lti_tool_proxies table. Must define every column to be compatible with mysqli. $select = "SELECT ltp.id, ltp.name, ltp.regurl, ltp.state, ltp.guid, ltp.secret, ltp.vendorcode, ltp.capabilityoffered, ltp.serviceoffered, ltp.toolproxy, ltp.createdby, ltp.timecreated, ltp.timemodified"; $sort = " ORDER BY ltp.name ASC, ltp.state DESC, ltp.timemodified DESC"; } $from = " FROM {lti_tool_proxies} ltp"; if ($orphanedonly) { $join = " LEFT JOIN {lti_types} lt ON ltp.id = lt.toolproxyid"; $where = " WHERE lt.toolproxyid IS null"; } else { $join = ""; $where = ""; } return $select . $from . $join . $where . $sort; } } classes/search/activity.php000064400000002577152265520610012032 0ustar00. /** * Search area for mod_lti activities. * * @package mod_lti * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\search; defined('MOODLE_INTERNAL') || die(); /** * Search area for mod_lti activities. * * @package mod_lti * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } classes/output/tool_configure_page.php000064400000004421152265520610014271 0ustar00. /** * Class containing data for tool_configure page * * @package mod_lti * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use moodle_url; use renderable; use templatable; use renderer_base; use stdClass; use help_icon; /** * Class containing data for tool_configure page * * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_configure_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $keyhelp = new help_icon('resourcekey', 'mod_lti'); $secrethelp = new help_icon('password', 'mod_lti'); $url = new moodle_url('/mod/lti/typessettings.php', array('sesskey' => sesskey(), 'returnto' => 'toolconfigure')); $data->configuremanualurl = $url->out(); $url = new moodle_url('/admin/settings.php?section=modsettinglti'); $data->managetoolsurl = $url->out(); $url = new moodle_url('/mod/lti/toolproxies.php'); $data->managetoolproxiesurl = $url->out(); $data->keyhelp = $keyhelp->export_for_template($output); $data->secrethelp = $secrethelp->export_for_template($output); return $data; } } classes/output/registration_upgrade_choice_page.php000064400000004531152265520610017010 0ustar00. /** * Class containing data for rendering LTI upgrade choices page. * * @copyright 2021 Cengage * @package mod_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for rendering LTI upgrade choices page. * * @copyright 2021 Cengage * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class registration_upgrade_choice_page implements renderable, templatable { /** @var array array of tools. */ protected array $tools = []; /** @var string tool URL. */ protected string $startregurl; /** * Constructor * * @param array $tools array of tools that can be upgraded * @param string $startregurl tool URL to start the registration process */ public function __construct(array $tools, string $startregurl) { $this->tools = $tools; $this->startregurl = $startregurl; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { $renderdata = new stdClass(); $renderdata->startregurlenc = urlencode($this->startregurl); $renderdata->sesskey = sesskey(); $renderdata->tools = []; foreach ($this->tools as $tool) { $renderdata->tools[] = (object)$tool; } return $renderdata; } } classes/output/repost_crosssite_page.php000064400000005346152265520610014674 0ustar00. /** * Render a page containing a simple form which reposts to self via JS. * * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking * cross-site cookies. * * @copyright 2021 Cengage * @package mod_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Render a page containing a simple form which reposts to self via JS. * * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking * cross-site cookies. * * @copyright 2021 Cengage * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repost_crosssite_page implements renderable, templatable { /** @var array POST params. */ protected $params; /** @var string URL to repost to. */ protected string $url; /** * Constructor * * @param string $url moodle URL to repost to * @param array $post the POST params to be re-posted */ public function __construct(string $url, array $post) { $this->params = array_map(function($k) use ($post) { return ["key" => $k, "value" => $post[$k]]; }, array_keys($post)); $this->url = $url; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { $renderdata = new stdClass(); $renderdata->url = $this->url; $renderdata->params = $this->params; return $renderdata; } } classes/output/course_tools_page_header.php000064400000004162152265520610015305 0ustar00. namespace mod_lti\output; use core\output\notification; use renderer_base; /** * Course tools page header renderable, containing the data for the page zero state and 'add tool' button. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_tools_page_header implements \templatable { /** * Constructor. * * @param int $courseid the course id. * @param int $toolcount the number of tools available in the course. * @param bool $canadd whether the user can add tools to the course or not. */ public function __construct(protected int $courseid, protected int $toolcount, protected bool $canadd) { } /** * Export the header's data for template use. * * @param renderer_base $output * @return object the data. */ public function export_for_template(renderer_base $output): \stdClass { $context = (object) []; if ($this->canadd) { $context->addlink = (new \moodle_url('/mod/lti/coursetooledit.php', ['course' => $this->courseid]))->out(); } if ($this->toolcount == 0) { $notification = new notification(get_string('nocourseexternaltoolsnotice', 'mod_lti'), notification::NOTIFY_INFO, true); $context->notoolsnotice = $notification->export_for_template($output); } return $context; } } classes/output/course_tools_page.php000064400000005502152265520610013774 0ustar00. namespace mod_lti\output; use core_reportbuilder\system_report_factory; use mod_lti\reportbuilder\local\systemreports\course_external_tools_list; /** * The course tools page renderable, containing a page header renderable and a course tools system report. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_tools_page implements \renderable { /** @var course_external_tools_list the course tools system report instance. */ protected course_external_tools_list $coursetoolsreport; /** @var course_tools_page_header the page header renderable instance. */ protected course_tools_page_header $coursetoolspageheader; /** * Renderable constructor. * * @param int $courseid the id of the course. */ public function __construct(int $courseid) { global $DB; $context = \context_course::instance($courseid); // Page intro, zero state and 'add new' button. $canadd = has_capability('mod/lti:addcoursetool', $context); $sql = 'SELECT COUNT(1) FROM {lti_types} tt WHERE tt.course IN(:siteid, :courseid) AND tt.coursevisible NOT IN(:coursevisible)'; $toolcount = $DB->count_records_sql($sql, ['siteid' => get_site()->id, 'courseid' => $courseid, 'coursevisible' => 0]); $this->coursetoolspageheader = new course_tools_page_header($courseid, $toolcount, $canadd); // Course tools report itself. $this->coursetoolsreport = system_report_factory::create(course_external_tools_list::class, $context); } /** * Get the course tools page header renderable. * * @return course_tools_page_header the renderable. */ public function get_header(): course_tools_page_header { return $this->coursetoolspageheader; } /** * Get the course tools list system report. * * @return course_external_tools_list the course tools list report. */ public function get_table(): course_external_tools_list { return $this->coursetoolsreport; } } classes/output/external_registration_return_page.php000064400000003140152265520610017263 0ustar00. /** * Class containing data for external registration return page. * * @package mod_lti * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for tool_configure page * * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_registration_return_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { return new stdClass(); } } classes/event/unknown_service_api_called.php000064400000005036152265520610015417 0ustar00. /** * The mod_lti unknown service api called event. * * @package mod_lti * @copyright 2013 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti unknown service api called event class. * * Event for when something happens with an unknown lti service API call. * * @package mod_lti * @since Moodle 2.6 * @copyright 2013 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class unknown_service_api_called extends \core\event\base { /** @var \stdClass Data to be used by event observers. */ protected $eventdata; /** * Sets custom data used by event observers. * * @param \stdClass $data */ public function set_message_data(\stdClass $data) { $this->eventdata = $data; } /** * Returns custom data for event observers. * * @return \stdClass */ public function get_message_data() { if ($this->is_restored()) { throw new \coding_exception('Function get_message_data() can not be used on restored events.'); } return $this->eventdata; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->context = \context_system::instance(); } /** * Returns localised description of what happened. * * @return string */ public function get_description() { return 'An unknown call to a service api was made.'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('ltiunknownserviceapicall', 'mod_lti'); } } classes/event/course_module_viewed.php000064400000003066152265520610014254 0ustar00. /** * The mod_lti course module viewed event. * * @package mod_lti * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti course module viewed event class. * * @package mod_lti * @since Moodle 2.7 * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'lti'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } public static function get_objectid_mapping() { return array('db' => 'lti', 'restore' => 'lti'); } } classes/event/course_module_instance_list_viewed.php000064400000002527152265520610017174 0ustar00. /** * The mod_lti instance list viewed event. * * @package mod_lti * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti instance list viewed event class. * * @package mod_lti * @since Moodle 2.7 * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No need for any code here as everything is handled by the parent class. } classes/service_exception_handler.php000064400000006236152265520610014140 0ustar00. /** * Exception handler for LTI services * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../locallib.php'); require_once(__DIR__.'/../servicelib.php'); /** * Handles exceptions when handling incoming LTI messages. * * Ensures that LTI always returns a XML message that can be consumed by the caller. * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class service_exception_handler { /** * Enable error response logging. * * @var bool */ protected $log = false; /** * The LTI service message ID, if known. * * @var string */ protected $id = ''; /** * The LTI service message type, if known. * * @var string */ protected $type = 'unknownRequest'; /** * Constructor. * * @param boolean $log Enable error response logging. */ public function __construct($log) { $this->log = $log; } /** * Set the LTI message ID being handled. * * @param string $id */ public function set_message_id($id) { if (!empty($id)) { $this->id = $id; } } /** * Set the LTI message type being handled. * * @param string $type */ public function set_message_type($type) { if (!empty($type)) { $this->type = $type; } } /** * Echo an exception message encapsulated in XML. * * @param \Exception|\Throwable $exception The exception that was thrown */ public function handle($exception) { $message = $exception->getMessage(); // Add the exception backtrace for developers. if (debugging('', DEBUG_DEVELOPER)) { $message .= "\n".format_backtrace(get_exception_info($exception)->backtrace, true); } // Switch to response. $type = str_replace('Request', 'Response', $this->type); // Build the appropriate xml. $response = lti_get_response_xml('failure', $message, $this->id, $type); $xml = $response->asXML(); // Log the request if necessary. if ($this->log) { lti_log_response($xml, $exception); } echo $xml; } } classes/task/clean_access_tokens.php000064400000003101152265520610013641 0ustar00. /** * A scheduled task for lti module. * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\task; use core\task\scheduled_task; defined('MOODLE_INTERNAL') || die(); /** * Class containing the scheduled task for lti module. * * @package mod_lti * @copyright 2018 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class clean_access_tokens extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('cleanaccesstokens', 'mod_lti'); } /** * Run lti cron. */ public function execute() { global $DB; $DB->delete_records_select('lti_access_tokens', 'validuntil < ?', [time()]); } } classes/analytics/indicator/cognitive_depth.php000064400000003303152265520610016033 0ustar00. /** * Cognitive depth indicator - lti. * * @package mod_lti * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - lti. * * @package mod_lti * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_lti'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_3; } } classes/analytics/indicator/activity_base.php000064400000002760152265520610015514 0ustar00. /** * Activity base class. * * @package mod_lti * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_lti * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * feedback_viewed_events * * @return string[] */ protected function feedback_viewed_events() { // Any view after the data graded counts as feedback viewed. return array('\mod_lti\event\course_module_viewed'); } } classes/analytics/indicator/social_breadth.php000064400000003267152265520610015634 0ustar00. /** * Social breadth indicator - lti. * * @package mod_lti * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - lti. * * @package mod_lti * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_lti'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_2; } } templates/loader.mustache000064400000002141152265520610011545 0ustar00{{! 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 . }} {{! @template mod_lti/loader This template provides the registration form to submit an external LTI registration. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { } }} {{#pix}} i/loading, core, {{#str}} loadinghelp, moodle {{/str}} {{/pix}} templates/tool_card.mustache000064400000014604152265520610012254 0ustar00{{! 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 . }} {{! @template mod_lti/tool_card This template provides the layout for a single tool card on the tool configuration page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * id * hascapabilitygroups * state ** pending|configured|rejected|unknown ** text * courseid * urls ** course - optional, required if courseid > 0 ** icon ** edit ** publickeyset ** accesstoken ** authrequest * name * description * platformid * clientid * deploymentid * instancecount Example context (json): { "id": 1, "name": "Example Tool", "description": "This is an example tool", "platformid": "https:\/\/moodle.example.com", "courseid": 0, "hascapabilitygroups": false, "state": { "text": "Active", "pending": false, "configured": true, "rejected": false, "unknown": false }, "urls": { "icon": "https://www.example.com/icon1.png", "edit": "https://www.example.com/mod/lti/typessettings.php", "publickeyset": "https:\/\/moodle.example.com\/mod\/lti\/certs.php", "accesstoken": "https:\/\/moodle.example.com\/mod\/lti\/token.php", "authrequest": "https:\/\/moodle.example.com\/mod\/lti\/auth.php" }, "clientid": null, "deploymentid": 1, "instancecount": 0 } }}
{{> mod_lti/loader }}
{{#pix}} i/valid, core, {{#str}} success {{/str}}{{/pix}}
{{#pix}} i/invalid, core, {{#str}} error {{/str}}{{/pix}}
{{#hascapabilitygroups}} {{> mod_lti/tool_type_capabilities_agree }} {{/hascapabilitygroups}}

{{description}}

{{> mod_lti/loader }}
{{#js}} require(['jquery', 'mod_lti/tool_card_controller'], function($, controller) { var cardElement = $('[data-type-id={{id}}]'); controller.init(cardElement); }); {{/js}} templates/cartridge_registration_form.mustache000064400000007074152265520610016072 0ustar00{{! 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 . }} {{! @template mod_lti/cartridge_registration_form This template provides the layout for the external tool configuration page in the LTI module. Classes required for JS: * lead * centered-menu * well * form-horizontal * text-center Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }}

{{#str}} enterkeyandsecret, mod_lti {{/str}}

{{#str}} enterkeyandsecret_help, mod_lti {{/str}}

{{#keyhelp}} {{>core/help_icon}} {{/keyhelp}}
{{#secrethelp}} {{>core/help_icon}} {{/secrethelp}}
{{#js}} require(['mod_lti/cartridge_registration_form'], function(registration) { registration.init(); }); {{/js}} templates/repost_crosssite.mustache000064400000002650152265520610013716 0ustar00{{! 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 . }} {{! @template mod_lti/repost_crosssite This template re-executes a cross site request but from the site domain, avoiding the cross site issue. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { "url":"https://some.tool.example/mod/lti/auth.php", "params": { "response_type": "id_token" } } }}
{{#params}} {{/params}}
{{#js}} document.getElementById("autopostme").submit(); {{/js}} templates/registration_upgrade_choice_page.mustache000064400000006150152265520610017032 0ustar00{{! 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 . }} {{! @template mod_lti/registration_upgrade_choice_page.mustache This template provides the layout to display the upgrade choices available when registering a tool for which there exists at least one other tool with the same domain. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * startregurlenc: the URL provided by the tool to start the registration, url encoded * tools: the list of tools matching the domain Example context (json): { "startregurlenc":"https%3A%2F%2Fmytool.example%2Fregister", "tools": [ { "id": 222, "name": "This is My LTI 1.1 tool", "baseurl": "https://mytool.example", "version": "LTI-1p0", "sesskey": "785t9302" } ] } }}

{{#str}}dynreg_update_text, mod_lti {{/str}}

{{#tools}} {{/tools}}
{{#str}}dynreg_update_name, mod_lti {{/str}} {{#str}}dynreg_update_url, mod_lti {{/str}} {{#str}}dynreg_update_version, mod_lti {{/str}}
{{name}} {{baseurl}} {{ltiversion}}
{{^tools}} {{#str}}dynreg_update_notools, mod_lti {{/str}} {{/tools}}
{{#js}} disableAndGo = e => { document.querySelectorAll("button").forEach(e=>e.setAttribute("disabled", true)); e.target.querySelector("span.btn-loader").style.display="inline"; let form = document.getElementById("startregform"); form.action = e.target.getAttribute("data-href"); form.submit(); }; document.querySelectorAll("button").forEach(b=>b.onclick=disableAndGo); {{/js}} templates/tool_config_modal_footer.mustache000064400000002576152265520610015347 0ustar00{{! 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 . }} {{! @template mod_lti/tool_config_modal_footer This template provides the layout for the footer of the configuration summary modal for a single tool opened from the tool configuration page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * mailto Example context (json): { "mailto": "mailto:?subject=...&body=..." } }}
templates/external_registration.mustache000064400000003457152265520610014726 0ustar00{{! 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 . }} {{! @template mod_lti/external_registration This template provides the layout for the external tool configuration page in the LTI module. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { } }}
{{#js}} require(['mod_lti/external_registration'], function(registration) { registration.init(); }); {{/js}} templates/tool_config_modal_body.mustache000064400000003777152265520610015012 0ustar00{{! 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 . }} {{! @template mod_lti/tool_config_modal_body This template provides the layout for the body of the configuration summary modal for a single tool opened from the tool configuration page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * platformid * clientid * deploymentid * urls ** publickeyset ** accesstoken ** authrequest Example context (json): { "platformid": "htps://...", "clientid": "123", "deploymentid": "1", "urls": { "publickeyset": "https://...", "accesstoken": "https://...", "authrequest": "https://..." } } }}
  • {{#str}} tooldetailsplatformid, mod_lti {{/str}}: {{platformid}}
  • {{#str}} tooldetailsclientid, mod_lti {{/str}}: {{clientid}}
  • {{#str}} tooldetailsdeploymentid, mod_lti {{/str}}: {{deploymentid}}
  • {{#str}} tooldetailspublickeyseturl, mod_lti {{/str}}: {{urls.publickeyset}}
  • {{#str}} tooldetailsaccesstokenurl, mod_lti {{/str}}: {{urls.accesstoken}}
  • {{#str}} tooldetailsauthrequesturl, mod_lti {{/str}}: {{urls.authrequest}}
templates/contentitem.mustache000064400000006476152265520610012647 0ustar00{{! 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 . }} {{! @template mod_lti/contentitem Provides a template for the creation of a new external tool instance via the content-item message. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * url The URL the iframe has to load. * postData The JSON object that contains the information to be POSTed for the ContentItemSelectionRequest. Example context (json): { "url": "/", "toolUrl": "http://external.tool.url", "postData": { "id": "1", "course": "1", "title": "Sample title", "text": "This is a description" } } }}
{{> mod_lti/loader }}

{{#str}} loadinghelp, moodle {{/str}}

{{#js}} require(['jquery'], function($) { var loadingContainer = $('.contentitem-loading-container'); var iframe = $('#contentitem-page-iframe'); var timeout = setTimeout(function () { var failedContainer = $('#tool-loading-failed'); failedContainer.removeClass('hidden'); }, 20000); // Submit form. $('#contentitem-request-form').submit(); iframe.on('load', function() { loadingContainer.addClass('hidden'); iframe.removeClass('hidden'); // Adjust iframe's width to the fit the container's width. var containerWidth = $('div.contentitem-container').width(); $('#contentitem-page-iframe').width(containerWidth + 'px'); // Adjust iframe's height to 75% of the width. var containerHeight = containerWidth * 0.75; $('#contentitem-page-iframe').height(containerHeight + 'px'); }); }); {{/js}} templates/tool_proxy_registration_form.mustache000064400000005163152265520610016341 0ustar00{{! 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 . }} {{! @template mod_lti/tool_proxy_registration_form This template provides the registration form to submit an external LTI registration. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * }}
{{> mod_lti/loader }}

{{#str}} loadinghelp, moodle {{/str}}

{{#js}} require(['jquery'], function($) { var loadingContainer = $('#tool-proxy-registration-form-container .registration-loading-container'); var iframe = $('#registration-page-iframe'); var timeout = setTimeout(function () { var failedContainer = $('#registration-loading-failed'); failedContainer.removeClass('hidden'); }, 20000); iframe.on('load', function() { loadingContainer.addClass('hidden'); iframe.removeClass('hidden'); }); }); {{/js}} templates/tool_list.mustache000064400000002347152265520610012317 0ustar00{{! 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 . }} {{! @template mod_lti/tool_list This template provides a list of LTI tools. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { } }}
{{^proxies}}{{^tools}}

{{#str}} no_lti_tools, mod_lti {{/str}}

{{/tools}}{{/proxies}} {{#proxies}} {{> mod_lti/tool_proxy_card }} {{/proxies}} {{#tools}} {{> mod_lti/tool_card }} {{/tools}}
templates/external_registration_return.mustache000064400000002525152265520610016320 0ustar00{{! 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 . }} {{! @template mod_lti/external_registration_return This template provides the layout for the external tool configuration page in the LTI module. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * }}
{{> mod_lti/loader }}

{{#str}} loadinghelp, moodle {{/str}}

{{#js}} require(['mod_lti/external_registration'], function(registration) { registration.init(); }); {{/js}} templates/course_tools_page_header.mustache000064400000003360152265520610015327 0ustar00{{! 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 . }} {{! @template mod_lti/course_tool_page_header Contains the page header for the 'LTI External tools' course page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * notoolsnotice Object exported notification, shown when the page has 0 tools available. * addlink String link to the add tool type page. Example context (json): { "notoolsnotice": { "message": "There are no LTI External tools yet", "extraclasses": "", "announce": false, "closebutton": true, "isinfo": true }, "addlink": "http://SITE/mod/lti/coursetooledit?course=45" } }}

{{#str}} courseexternaltoolsinfo, mod_lti {{/str}}

{{#notoolsnotice}} {{> core/notification_info}} {{/notoolsnotice}} {{#addlink}} {{/addlink}}
templates/tool_proxy_card.mustache000064400000011171152265520610013511 0ustar00{{! 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 . }} {{! @template mod_lti/tool_proxy_card This template provides the layout for a single tool card on the tool configuration page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * hascapabilitygroups * state ** pending|configured|rejected|unknown ** text * courseid * urls ** course ** icon * name * description * instancecount Example context (json): { "id": 1, "name": "Test Tool", "urls": { "edit": "https://moodle.example.com/mod/lti/registersettings.php", "icon": "https://moodle.example.com/theme/image.php/boost/lti/-1/icon" }, "state": { "text": "Accepted", "configured": false, "pending": false, "rejected": false, "accepted": true, "unknown": false } } }}
{{> mod_lti/loader }}
{{#pix}} i/valid, core, {{#str}} success {{/str}}{{/pix}}
{{#pix}} i/invalid, core, {{#str}} error {{/str}}{{/pix}}
{{#hascapabilitygroups}} {{> mod_lti/tool_type_capabilities_agree }} {{/hascapabilitygroups}}

{{description}}

{{> mod_lti/loader }}
{{#js}} require(['jquery', 'mod_lti/tool_proxy_card_controller'], function($, controller) { var cardElement = $('[data-proxy-id={{id}}]'); controller.init(cardElement); }); {{/js}} templates/categorynode.mustache000064400000007016152265520610012770 0ustar00{{! 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 . }} {{! @template mod_lti/categorynode Provides a template for the course categories tree view. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * id Course category ID. * parent Course category parent ID. * name Course category name. * haschildren Course category has any sub-category - true/false. Example context (json): { "id": "5", "parent": "0", "name": "Example 1", "nodes": [{ "id": "6", "parent": "5", "name": "Sub category 1", "nodes": [{ "id": "7", "parent": "6", "name": "Sub Sub category 1", "nodes": [], "haschildren": "0" }], "haschildren": "1" }, { "id": "8", "parent": "5", "name": "Sub category 2", "nodes": [{ "id": "9", "parent": "8", "name": "Sub Sub category 2", "nodes": [], "haschildren": "0" }], "haschildren": "1" }, { "id": "593", "parent": "5", "name": "Sub category 3", "nodes": [], "haschildren": "1" } ], "haschildren": "1" } }} {{^id}}
{{/id}} {{#nodes}} {{#haschildren}}
{{>mod_lti/categorynode}}
{{/haschildren}} {{^haschildren}}

{{/haschildren}} {{/nodes}} {{^id}}
{{/id}}templates/tool_deeplinking_results.mustache000064400000003271152265520610015413 0ustar00{{! 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 . }} {{! @template mod_lti/tool_deeplinking_results This template lists the items that will be added to the course after a deep linking flow. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { "items": [ { "name": "Chapter 1", "instructorchoiceacceptgrades": 0 }, { "name": "Quiz 1", "instructorchoiceacceptgrades": 1, "grade_modgrade_point": 20.5 } ] } }}

{{#str}} contentitem_multiple_description, mod_lti {{/str}}

    {{#items}}
  • {{{name}}} {{#instructorchoiceacceptgrades}} {{#str}} contentitem_multiple_graded , mod_lti, {{{grade_modgrade_point}}} {{/str}} {{/instructorchoiceacceptgrades}}
  • {{/items}}
templates/tool_configure.mustache000064400000007671152265520610013332 0ustar00{{! 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 . }} {{! @template mod_lti/tool_configure This template provides the layout for the external tool configuration page in the LTI module. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * Example context (json): { "configuremanualurl":"https://some.tool.example/mod/lti/typessettings.php?sesskey=OKl37bHflL&returnto=toolconfigure", "managetoolsurl":"https://some.tool.example/admin/settings.php?section=modsettinglti", "managetoolproxiesurl":"https://some.tool.example/mod/lti/toolproxies.php" } }}

{{#str}} manage_external_tools, mod_lti {{/str}}

{{#str}} autoaddtype, mod_lti {{/str}}

{{#str}} manuallyaddtype, mod_lti, {{{configuremanualurl}}} {{/str}}

{{#str}} manage_tools, mod_lti {{/str}}
{{#str}} manage_tool_proxies, mod_lti {{/str}}

{{#str}} tooltypes, mod_lti {{/str}}

{{> mod_lti/loader }}
{{> mod_lti/tool_list }}
{{#js}} require(['mod_lti/tool_configure_controller'], function(controller) { controller.init(); }); {{/js}} templates/tool_type_capabilities_agree.mustache000064400000004774152265520610016207 0ustar00{{! 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 . }} {{! @template mod_lti/tool_type_capabilities_agree List of tool type's capabilities to be agreed to. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * }}

{{#str}} capabilitiesrequired, mod_lti {{/str}}

    {{#capabilitygroups}}
  • {{.}}
  • {{/capabilitygroups}}

{{#str}} confirmtoolactivation, mod_lti {{/str}}

{{#js}} require(['jquery', 'mod_lti/events', 'mod_lti/keys'], function($, ltiEvents, KEYS) { var container = $('.capabilities-container.tool-type-{{id}}'); var yesButton = container.find('button.yes'); var noButton = container.find('button.no'); yesButton.click(function(e) { e.preventDefault(); container.trigger(ltiEvents.CAPABILITIES_AGREE); }); yesButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); yesButton.click(); } } }); noButton.click(function(e) { e.preventDefault(); container.trigger(ltiEvents.CAPABILITIES_DECLINE); }); noButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); noButton.click(); } } }); }); {{/js}} upgrade.txt000064400000011642152265520610006744 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the lti code. === 4.3 === * The `lti_libxml_disable_entity_loader` method is deprecated, as it is no longer required from PHP 8.0 * The `mod_lti_mod_form` constructor will now throw an exception if called without passing a typeid as manual configuration of instances is now unsupported. * The `lti_get_lti_types_by_course` method is deprecated. Please use mod_lti\local\types_helper::get_lti_types_by_course instead. * The capability `mod/lti:addmanualinstance` is now deprecated. Since manual instance creation is no longer supported, there is no substitute for this capability. === 4.2 === * get_jwt_claim_mappings() function has been added to service_base class. It allows a service subplugin to be compatible with LTI 1.3 by exposing its 1.1 custom parameters to LTI 1.3 claim mapping. * The `serialise_tool_proxy` method, deprecated since 3.10, has been removed and can no longer be used === 4.1 === * The callback get_shortcuts() is now removed. Please use get_course_content_items and get_all_content_items instead. * Services can now implement the override_endpoint function to replace the launch url and/or the custom parameters. === 3.10 === * Select Content supports multiple, allowing a tool to return more than one link at a time. Parameter multiple in function lti_build_content_item_selection_request() is now set to true. * Deprecated unused function after external function, 'get_tool_proxies()', was refactored: - serialise_tool_proxy() === 3.8 === * The following functions have been finally deprecated and can not be used anymore: * lti_scale_used() === 3.7 === * Deprecated functions to add support for LTI 1 tools to access services: - mod_lti\local\ltiservice\service_base->check_tool_proxy() - mod_lti\local\ltiservice\service_base->check_type() - mod_lti\local\ltiservice\resource_base->check_tool_proxy() - mod_lti\local\ltiservice\resource_base->check_type() - mod_lti_edit_types_form->get_lti_advantage_services() replaced by: - mod_lti\local\ltiservice\service_base->check_tool() - mod_lti\local\ltiservice\resource_base->check_tool() - mod_lti_edit_types_form->get_lti_services() * Deprecated unused function: - mod_lti\local\ltiservice\service_base->get_configuration_parameter_names() * Deprecated functions to simplify implementation of LTI scopes for accessing services: - mod_lti\local\ltiservice\resource_base->get_permissions() - ltiservice_gradebookservices\local\resources\lineitem\get_permissions() - ltiservice_gradebookservices\local\resources\lineitems\get_permissions() - ltiservice_gradebookservices\local\resources\results\get_permissions() - ltiservice_gradebookservices\local\resources\scores\get_permissions() - ltiservice_memberships\local\resources\contextmemberships\get_permissions() replaced by: - mod_lti\local\ltiservice\service_base->get_permitted_scopes() - ltiservice_gradebookservices\local\service\gradebookservices->get_permitted_scopes() - ltiservice_memberships\local\service\memberships\->get_permitted_scopes() * Deprecated function to remove definition as static: - ltiservice_memberships\local\service\memberships\get_users_json() replaced by: - ltiservice_memberships\local\service\memberships\get_members_json() === 3.4 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-57432 for more info: - mod_lti_upgrade_custom_separator() === 3.2 === * Support for LTI ContentItem message type. * New function lti_build_content_item_selection_request() for building an LTI ContentItemSelectionRequest. * New Select content button in External tool configuration page that lets the user configure an external tool by fetching tool configuration content from the preconfigured tool's content selection interface. === 3.1 === * When requesting a grade from the outcome service, and empty grade will now be returned as an empty grade in accordance with the spec. Previously a grade of 0 would be returned. === 3.0 === * Function lti_view (locallib.php) was renamed to lti_launch_tool. A new and different lti_view function was created in the module lib.php containing the code for launching events and update the completion status for the module. === 2.8 === * Support for LTI 2 added, including extensible services implemented as ltiservice plugins. * Function sendOAuthBodyPOST removed from OAuthBody.php because it was not being used. * The field 'grade' in the table {lti} is now an integer rather than a numeric to bring it in line with the 'grade' field in other activities. === 2.7 === * mod_lti\event\unknown_service_api_called now has less data stored in 'other' but everything is still available for event observers via method get_message_data() locallib.php000064400000476500152265520610007056 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains the library of functions and constants for the lti module * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; // TODO: Switch to core oauthlib once implemented - MDL-30149. use mod_lti\helper; use moodle\mod\lti as lti; use Firebase\JWT\JWT; use Firebase\JWT\JWK; use Firebase\JWT\Key; use mod_lti\local\ltiopenid\jwks_helper; use mod_lti\local\ltiopenid\registration_helper; global $CFG; require_once($CFG->dirroot.'/mod/lti/OAuth.php'); require_once($CFG->libdir.'/weblib.php'); require_once($CFG->dirroot . '/course/modlib.php'); require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i'); define('LTI_LAUNCH_CONTAINER_DEFAULT', 1); define('LTI_LAUNCH_CONTAINER_EMBED', 2); define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3); define('LTI_LAUNCH_CONTAINER_WINDOW', 4); define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5); define('LTI_TOOL_STATE_ANY', 0); define('LTI_TOOL_STATE_CONFIGURED', 1); define('LTI_TOOL_STATE_PENDING', 2); define('LTI_TOOL_STATE_REJECTED', 3); define('LTI_TOOL_PROXY_TAB', 4); define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1); define('LTI_TOOL_PROXY_STATE_PENDING', 2); define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3); define('LTI_TOOL_PROXY_STATE_REJECTED', 4); define('LTI_SETTING_NEVER', 0); define('LTI_SETTING_ALWAYS', 1); define('LTI_SETTING_DELEGATE', 2); define('LTI_COURSEVISIBLE_NO', 0); define('LTI_COURSEVISIBLE_PRECONFIGURED', 1); define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2); define('LTI_VERSION_1', 'LTI-1p0'); define('LTI_VERSION_2', 'LTI-2p0'); define('LTI_VERSION_1P3', '1.3.0'); define('LTI_RSA_KEY', 'RSA_KEY'); define('LTI_JWK_KEYSET', 'JWK_KEYSET'); define('LTI_DEFAULT_ORGID_SITEID', 'SITEID'); define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST'); define('LTI_ACCESS_TOKEN_LIFE', 3600); // Standard prefix for JWT claims. define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti'); /** * Return the mapping for standard message types to JWT message_type claim. * * @return array */ function lti_get_jwt_message_type_mapping() { return array( 'basic-lti-launch-request' => 'LtiResourceLinkRequest', 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest', 'LtiDeepLinkingResponse' => 'ContentItemSelection', 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest', ); } /** * Return the mapping for standard message parameters to JWT claim. * * @return array */ function lti_get_jwt_claim_mapping() { $mapping = []; $services = lti_get_services(); foreach ($services as $service) { $mapping = array_merge($mapping, $service->get_jwt_claim_mappings()); } $mapping = array_merge($mapping, [ 'accept_copy_advice' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_copy_advice', 'isarray' => false, 'type' => 'boolean' ], 'accept_media_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_media_types', 'isarray' => true ], 'accept_multiple' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_multiple', 'isarray' => false, 'type' => 'boolean' ], 'accept_presentation_document_targets' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_presentation_document_targets', 'isarray' => true ], 'accept_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_types', 'isarray' => true ], 'accept_unsigned' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_unsigned', 'isarray' => false, 'type' => 'boolean' ], 'auto_create' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'auto_create', 'isarray' => false, 'type' => 'boolean' ], 'can_confirm' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'can_confirm', 'isarray' => false, 'type' => 'boolean' ], 'content_item_return_url' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'deep_link_return_url', 'isarray' => false ], 'content_items' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isarray' => true ], 'data' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'data', 'isarray' => false ], 'text' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'text', 'isarray' => false ], 'title' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'title', 'isarray' => false ], 'lti_msg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'msg', 'isarray' => false ], 'lti_log' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'log', 'isarray' => false ], 'lti_errormsg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errormsg', 'isarray' => false ], 'lti_errorlog' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errorlog', 'isarray' => false ], 'context_id' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'id', 'isarray' => false ], 'context_label' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'label', 'isarray' => false ], 'context_title' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'title', 'isarray' => false ], 'context_type' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'type', 'isarray' => true ], 'for_user_id' => [ 'suffix' => '', 'group' => 'for_user', 'claim' => 'user_id', 'isarray' => false ], 'lis_course_offering_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_offering_sourcedid', 'isarray' => false ], 'lis_course_section_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_section_sourcedid', 'isarray' => false ], 'launch_presentation_css_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'css_url', 'isarray' => false ], 'launch_presentation_document_target' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'document_target', 'isarray' => false ], 'launch_presentation_height' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'height', 'isarray' => false ], 'launch_presentation_locale' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'locale', 'isarray' => false ], 'launch_presentation_return_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'return_url', 'isarray' => false ], 'launch_presentation_width' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'width', 'isarray' => false ], 'lis_person_contact_email_primary' => [ 'suffix' => '', 'group' => null, 'claim' => 'email', 'isarray' => false ], 'lis_person_name_family' => [ 'suffix' => '', 'group' => null, 'claim' => 'family_name', 'isarray' => false ], 'lis_person_name_full' => [ 'suffix' => '', 'group' => null, 'claim' => 'name', 'isarray' => false ], 'lis_person_name_given' => [ 'suffix' => '', 'group' => null, 'claim' => 'given_name', 'isarray' => false ], 'lis_person_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'person_sourcedid', 'isarray' => false ], 'user_id' => [ 'suffix' => '', 'group' => null, 'claim' => 'sub', 'isarray' => false ], 'user_image' => [ 'suffix' => '', 'group' => null, 'claim' => 'picture', 'isarray' => false ], 'roles' => [ 'suffix' => '', 'group' => '', 'claim' => 'roles', 'isarray' => true ], 'role_scope_mentor' => [ 'suffix' => '', 'group' => '', 'claim' => 'role_scope_mentor', 'isarray' => false ], 'deployment_id' => [ 'suffix' => '', 'group' => '', 'claim' => 'deployment_id', 'isarray' => false ], 'lti_message_type' => [ 'suffix' => '', 'group' => '', 'claim' => 'message_type', 'isarray' => false ], 'lti_version' => [ 'suffix' => '', 'group' => '', 'claim' => 'version', 'isarray' => false ], 'resource_link_description' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'description', 'isarray' => false ], 'resource_link_id' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'id', 'isarray' => false ], 'resource_link_title' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'title', 'isarray' => false ], 'tool_consumer_info_product_family_code' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'product_family_code', 'isarray' => false ], 'tool_consumer_info_version' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'version', 'isarray' => false ], 'tool_consumer_instance_contact_email' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'contact_email', 'isarray' => false ], 'tool_consumer_instance_description' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'description', 'isarray' => false ], 'tool_consumer_instance_guid' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'guid', 'isarray' => false ], 'tool_consumer_instance_name' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'name', 'isarray' => false ], 'tool_consumer_instance_url' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'url', 'isarray' => false ] ]); return $mapping; } /** * Return the type of the instance, using domain matching if no explicit type is set. * * @param object $instance the external tool activity settings * @return object|null * @since Moodle 3.9 */ function lti_get_instance_type(object $instance): ?object { if (empty($instance->typeid)) { if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) { $tool = lti_get_tool_by_url_match($instance->securetoolurl, $instance->course); } return $tool; } return lti_get_type($instance->typeid); } /** * Return the launch data required for opening the external tool. * * @param stdClass $instance the external tool activity settings * @param string $nonce the nonce value to use (applies to LTI 1.3 only) * @return array the endpoint URL and parameters (including the signature) * @since Moodle 3.0 */ function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) { global $PAGE, $USER; $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request'; $tool = lti_get_instance_type($instance); if ($tool) { $typeid = $tool->id; $ltiversion = $tool->ltiversion; } else { $typeid = null; $ltiversion = LTI_VERSION_1; } if ($typeid) { $typeconfig = lti_get_type_config($typeid); } else { // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults. $typeconfig = (array)$instance; $typeconfig['sendname'] = $instance->instructorchoicesendname; $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr; $typeconfig['customparameters'] = $instance->instructorcustomparameters; $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades; $typeconfig['allowroster'] = $instance->instructorchoiceallowroster; $typeconfig['forcessl'] = '0'; } if (isset($tool->toolproxyid)) { $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $toolproxy = null; if (!empty($instance->resourcekey)) { $key = $instance->resourcekey; } else if ($ltiversion === LTI_VERSION_1P3) { $key = $tool->clientid; } else if (!empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } else { $key = ''; } if (!empty($instance->password)) { $secret = $instance->password; } else if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } else { $secret = ''; } } $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; $endpoint = trim($endpoint); // If the current request is using SSL and a secure tool URL is specified, use it. if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) { $endpoint = trim($instance->securetoolurl); } // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL. if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { if (!empty($instance->securetoolurl)) { $endpoint = trim($instance->securetoolurl); } if ($endpoint !== '') { $endpoint = lti_ensure_url_is_https($endpoint); } } else if ($endpoint !== '' && !strstr($endpoint, '://')) { $endpoint = 'http://' . $endpoint; } $orgid = lti_get_organizationid($typeconfig); $course = $PAGE->course; $islti2 = isset($tool->toolproxyid); $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid); if ($islti2) { $requestparams = lti_build_request_lti2($tool, $allparams); } else { $requestparams = $allparams; } $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype)); $customstr = ''; if (isset($typeconfig['customparameters'])) { $customstr = $typeconfig['customparameters']; } $services = lti_get_services(); foreach ($services as $service) { [$endpoint, $customstr] = $service->override_endpoint($messagetype, $endpoint, $customstr, $instance->course, $instance); } $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, $instance->instructorcustomparameters, $islti2)); $launchcontainer = lti_get_launch_container($instance, $typeconfig); $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id, 'sesskey' => sesskey()); // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. $url = new \moodle_url('/mod/lti/return.php', $returnurlparams); $returnurl = $url->out(false); if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { $returnurl = lti_ensure_url_is_https($returnurl); } $target = ''; switch($launchcontainer) { case LTI_LAUNCH_CONTAINER_EMBED: case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS: $target = 'iframe'; break; case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW: $target = 'frame'; break; case LTI_LAUNCH_CONTAINER_WINDOW: $target = 'window'; break; } if (!empty($target)) { $requestparams['launch_presentation_document_target'] = $target; } $requestparams['launch_presentation_return_url'] = $returnurl; // Add the parameters configured by the LTI services. if ($typeid && !$islti2) { $services = lti_get_services(); foreach ($services as $service) { $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request', $course->id, $USER->id , $typeid, $instance->id); foreach ($serviceparameters as $paramkey => $paramvalue) { $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue, $islti2); } } } // Allow request params to be updated by sub-plugins. $plugins = core_component::get_plugin_list('ltisource'); foreach (array_keys($plugins) as $plugin) { $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch', array($instance, $endpoint, $requestparams), array()); if (!empty($pluginparams) && is_array($pluginparams)) { $requestparams = array_merge($requestparams, $pluginparams); } } if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) { if ($ltiversion !== LTI_VERSION_1P3) { $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret); } else { $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce); } $endpointurl = new \moodle_url($endpoint); $endpointparams = $endpointurl->params(); // Strip querystring params in endpoint url from $parms to avoid duplication. if (!empty($endpointparams) && !empty($parms)) { foreach (array_keys($endpointparams) as $paramname) { if (isset($parms[$paramname])) { unset($parms[$paramname]); } } } } else { // If no key and secret, do the launch unsigned. $returnurlparams['unsigned'] = '1'; $parms = $requestparams; } return array($endpoint, $parms); } /** * Launch an external tool activity. * * @param stdClass $instance the external tool activity settings * @param int $foruserid for user param, optional * @return string The HTML code containing the javascript code for the launch */ function lti_launch_tool($instance, $foruserid=0) { list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid); $debuglaunch = ( $instance->debuglaunch == 1 ); $content = lti_post_launch_html($parms, $endpoint, $debuglaunch); echo $content; } /** * Prepares an LTI registration request message * * @param object $toolproxy Tool Proxy instance object */ function lti_register($toolproxy) { $endpoint = $toolproxy->regurl; // Change the status to pending. $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING; lti_update_tool_proxy($toolproxy); $requestparams = lti_build_registration_request($toolproxy); $content = lti_post_launch_html($requestparams, $endpoint, false); echo $content; } /** * Gets the parameters for the regirstration request * * @param object $toolproxy Tool Proxy instance object * @return array Registration request parameters */ function lti_build_registration_request($toolproxy) { $key = $toolproxy->guid; $secret = $toolproxy->secret; $requestparams = array(); $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest'; $requestparams['lti_version'] = 'LTI-2p0'; $requestparams['reg_key'] = $key; $requestparams['reg_password'] = $secret; $requestparams['reg_url'] = $toolproxy->regurl; // Add the profile URL. $profileservice = lti_get_service_by_name('profile'); $profileservice->set_tool_proxy($toolproxy); $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url'); // Add the return URL. $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey()); $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams); $returnurl = $url->out(false); $requestparams['launch_presentation_return_url'] = $returnurl; return $requestparams; } /** get Organization ID using default if no value provided * @param object $typeconfig * @return string */ function lti_get_organizationid($typeconfig) { global $CFG; // Default the organizationid if not specified. if (empty($typeconfig['organizationid'])) { if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) { $urlparts = parse_url($CFG->wwwroot); return $urlparts['host']; } else { return md5(get_site_identifier()); } } return $typeconfig['organizationid']; } /** * Build source ID * * @param int $instanceid * @param int $userid * @param string $servicesalt * @param null|int $typeid * @param null|int $launchid * @return stdClass */ function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) { $data = new \stdClass(); $data->instanceid = $instanceid; $data->userid = $userid; $data->typeid = $typeid; if (!empty($launchid)) { $data->launchid = $launchid; } else { $data->launchid = mt_rand(); } $json = json_encode($data); $hash = hash('sha256', $json . $servicesalt, false); $container = new \stdClass(); $container->data = $data; $container->hash = $hash; return $container; } /** * This function builds the request that must be sent to the tool producer * * @param object $instance Basic LTI instance object * @param array $typeconfig Basic LTI tool configuration * @param object $course Course object * @param int|null $typeid Basic LTI tool ID * @param boolean $islti2 True if an LTI 2 tool is being launched * @param string $messagetype LTI Message Type for this launch * @param int $foruserid User targeted by this launch * * @return array Request details */ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false, $messagetype = 'basic-lti-launch-request', $foruserid = 0) { global $USER, $CFG; if (empty($instance->cmid)) { $instance->cmid = 0; } $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2); $requestparams = array( 'user_id' => $USER->id, 'lis_person_sourcedid' => $USER->idnumber, 'roles' => $role, 'context_id' => $course->id, 'context_label' => trim(html_to_text($course->shortname, 0)), 'context_title' => trim(html_to_text($course->fullname, 0)), ); if ($foruserid) { $requestparams['for_user_id'] = $foruserid; } if ($messagetype) { $requestparams['lti_message_type'] = $messagetype; } if (!empty($instance->name)) { $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0)); } if (!empty($instance->cmid)) { $intro = format_module_intro('lti', $instance, $instance->cmid); $intro = trim(html_to_text($intro, 0, false)); // This may look weird, but this is required for new lines // so we generate the same OAuth signature as the tool provider. $intro = str_replace("\n", "\r\n", $intro); $requestparams['resource_link_description'] = $intro; } if (!empty($instance->id)) { $requestparams['resource_link_id'] = $instance->id; } if (!empty($instance->resource_link_id)) { $requestparams['resource_link_id'] = $instance->resource_link_id; } if ($course->format == 'site') { $requestparams['context_type'] = 'Group'; } else { $requestparams['context_type'] = 'CourseSection'; $requestparams['lis_course_section_sourcedid'] = $course->idnumber; } if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 || $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS)) ) { $placementsecret = $instance->servicesalt; $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); $requestparams['lis_result_sourcedid'] = $sourcedid; // Add outcome service URL. $serviceurl = new \moodle_url('/mod/lti/service.php'); $serviceurl = $serviceurl->out(); $forcessl = false; if (!empty($CFG->mod_lti_forcessl)) { $forcessl = true; } if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) { $serviceurl = lti_ensure_url_is_https($serviceurl); } $requestparams['lis_outcome_service_url'] = $serviceurl; } // Send user's name and email data if appropriate. if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS || ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname) && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS) ) { $requestparams['lis_person_name_given'] = $USER->firstname; $requestparams['lis_person_name_family'] = $USER->lastname; $requestparams['lis_person_name_full'] = fullname($USER); $requestparams['ext_user_username'] = $USER->username; } if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS || ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr) && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS) ) { $requestparams['lis_person_contact_email_primary'] = $USER->email; } return $requestparams; } /** * This function builds the request that must be sent to an LTI 2 tool provider * * @param object $tool Basic LTI tool object * @param array $params Custom launch parameters * * @return array Request details */ function lti_build_request_lti2($tool, $params) { $requestparams = array(); $capabilities = lti_get_capabilities(); $enabledcapabilities = explode("\n", $tool->enabledcapability); foreach ($enabledcapabilities as $capability) { if (array_key_exists($capability, $capabilities)) { $val = $capabilities[$capability]; if ($val && (substr($val, 0, 1) != '$')) { if (isset($params[$val])) { $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]]; } } } } return $requestparams; } /** * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer * * @param stdClass $instance Basic LTI instance object * @param string $orgid Organisation ID * @param boolean $islti2 True if an LTI 2 tool is being launched * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Request details * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see lti_build_standard_message() */ function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') { if (!$islti2) { $ltiversion = LTI_VERSION_1; } else { $ltiversion = LTI_VERSION_2; } return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype); } /** * This function builds the standard parameters for an LTI message that must be sent to the tool producer * * @param stdClass $instance Basic LTI instance object * @param string $orgid Organisation ID * @param boolean $ltiversion LTI version to be used for tool messages * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Message parameters */ function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') { global $CFG; $requestparams = array(); if ($instance) { $requestparams['resource_link_id'] = $instance->id; if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { $requestparams['resource_link_id'] = $instance->resource_link_id; } } $requestparams['launch_presentation_locale'] = current_language(); // Make sure we let the tool know what LMS they are being called from. $requestparams['ext_lms'] = 'moodle-2'; $requestparams['tool_consumer_info_product_family_code'] = 'moodle'; $requestparams['tool_consumer_info_version'] = strval($CFG->version); // Add oauth_callback to be compliant with the 1.0A spec. $requestparams['oauth_callback'] = 'about:blank'; $requestparams['lti_version'] = $ltiversion; $requestparams['lti_message_type'] = $messagetype; if ($orgid) { $requestparams["tool_consumer_instance_guid"] = $orgid; } if (!empty($CFG->mod_lti_institution_name)) { $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0)); } else { $requestparams['tool_consumer_instance_name'] = get_site()->shortname; } $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0)); return $requestparams; } /** * This function builds the custom parameters * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param object $instance Tool placement instance object * @param array $params LTI launch parameters * @param string $customstr Custom parameters defined for tool * @param string $instructorcustomstr Custom parameters defined for this placement * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return array Custom parameters */ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) { // Concatenate the custom parameters from the administrator and the instructor // Instructor parameters are only taken into consideration if the administrator // has given permission. $custom = array(); if ($customstr) { $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2); } if ($instructorcustomstr) { $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $instructorcustomstr, $islti2), $custom); } if ($islti2) { $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $tool->parameter, true), $custom); $settings = lti_get_tool_settings($tool->toolproxyid); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); if (!empty($instance->course)) { $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); if (!empty($instance->id)) { $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); } } } return $custom; } /** * Builds a standard LTI Content-Item selection request. * * @param int $id The tool type ID. * @param stdClass $course The course object. * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP) * will use to return the Content-Item message. * @param string $title The tool's title, if available. * @param string $text The text to display to represent the content item. This value may be a long description of the content item. * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default. * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened * (via the presentationDocumentTarget element for a returned content item). * If empty, "frame", "iframe", and "window" will be supported by default. * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default. * any option for the user to cancel the operation. False by default. * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not. * A signed message should always be required when the content item is being created automatically in the * TC without further interaction from the user. False by default. * @param bool $canconfirm Flag for can_confirm parameter. False by default. * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default. * @param string $nonce * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface. * @throws moodle_exception When the LTI tool type does not exist.` * @throws coding_exception For invalid media type and presentation target parameters. */ function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [], $presentationtargets = [], $autocreate = false, $multiple = true, $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') { global $USER; $tool = lti_get_type($id); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } if (!is_array($mediatypes)) { throw new coding_exception('The list of accepted media types should be in an array'); } if (!is_array($presentationtargets)) { throw new coding_exception('The list of accepted presentation targets should be in an array'); } // Check title. If empty, use the tool's name. if (empty($title)) { $title = $tool->name; } $typeconfig = lti_get_type_config($id); $key = ''; $secret = ''; $islti2 = false; $islti13 = false; if (isset($tool->toolproxyid)) { $islti2 = true; $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $islti13 = $tool->ltiversion === LTI_VERSION_1P3; $toolproxy = null; if ($islti13 && !empty($tool->clientid)) { $key = $tool->clientid; } else if (!$islti13 && !empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } } $tool->enabledcapability = ''; if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) { $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest']; } $tool->parameter = ''; if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) { $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest']; } // Set the tool URL. if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) { $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']); } else { $toolurl = new moodle_url($typeconfig['toolurl']); } // Check if SSL is forced. if (!empty($typeconfig['forcessl'])) { // Make sure the tool URL is set to https. if (strtolower($toolurl->get_scheme()) === 'http') { $toolurl->set_scheme('https'); } // Make sure the return URL is set to https. if (strtolower($returnurl->get_scheme()) === 'http') { $returnurl->set_scheme('https'); } } $toolurlout = $toolurl->out(false); // Get base request parameters. $instance = new stdClass(); $instance->course = $course->id; $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2); // Get LTI2-specific request parameters and merge to the request parameters if applicable. if ($islti2) { $lti2params = lti_build_request_lti2($tool, $requestparams); $requestparams = array_merge($requestparams, $lti2params); } // Get standard request parameters and merge to the request parameters. $orgid = lti_get_organizationid($typeconfig); $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest'); $requestparams = array_merge($requestparams, $standardparams); // Get custom request parameters and merge to the request parameters. $customstr = ''; if (!empty($typeconfig['customparameters'])) { $customstr = $typeconfig['customparameters']; } $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2); $requestparams = array_merge($requestparams, $customparams); // Add the parameters configured by the LTI services. if ($id && !$islti2) { $services = lti_get_services(); foreach ($services as $service) { $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest', $course->id, $USER->id , $id); foreach ($serviceparameters as $paramkey => $paramvalue) { $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue, $islti2); } } } // Allow request params to be updated by sub-plugins. $plugins = core_component::get_plugin_list('ltisource'); foreach (array_keys($plugins) as $plugin) { $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []); if (!empty($pluginparams) && is_array($pluginparams)) { $requestparams = array_merge($requestparams, $pluginparams); } } if (!$islti13) { // Media types. Set to ltilink by default if empty. if (empty($mediatypes)) { $mediatypes = [ 'application/vnd.ims.lti.v1.ltilink', ]; } $requestparams['accept_media_types'] = implode(',', $mediatypes); } else { // Only LTI links are currently supported. $requestparams['accept_types'] = 'ltiResourceLink'; } // Presentation targets. Supports frame, iframe, window by default if empty. if (empty($presentationtargets)) { $presentationtargets = [ 'frame', 'iframe', 'window', ]; } $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets); // Other request parameters. $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false'; $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false'; $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false'; $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false'; $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false'; $requestparams['content_item_return_url'] = $returnurl->out(false); $requestparams['title'] = $title; $requestparams['text'] = $text; if (!$islti13) { $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret); } else { $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce); } $toolurlparams = $toolurl->params(); // Strip querystring params in endpoint url from $signedparams to avoid duplication. if (!empty($toolurlparams) && !empty($signedparams)) { foreach (array_keys($toolurlparams) as $paramname) { if (isset($signedparams[$paramname])) { unset($signedparams[$paramname]); } } } // Check for params that should not be passed. Unset if they are set. $unwantedparams = [ 'resource_link_id', 'resource_link_title', 'resource_link_description', 'launch_presentation_return_url', 'lis_result_sourcedid', ]; foreach ($unwantedparams as $param) { if (isset($signedparams[$param])) { unset($signedparams[$param]); } } // Prepare result object. $result = new stdClass(); $result->params = $signedparams; $result->url = $toolurlout; return $result; } /** * Verifies the OAuth signature of an incoming message. * * @param int $typeid The tool type ID. * @param string $consumerkey The consumer key. * @return stdClass Tool type * @throws moodle_exception * @throws lti\OAuthException */ function lti_verify_oauth_signature($typeid, $consumerkey) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } $typeconfig = lti_get_type_config($typeid); if (isset($tool->toolproxyid)) { $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $toolproxy = null; if (!empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } else { $key = ''; } if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } else { $secret = ''; } } if ($consumerkey !== $key) { throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); } $store = new lti\TrivialOAuthDataStore(); $store->add_consumer($key, $secret); $server = new lti\OAuthServer($store); $method = new lti\OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = lti\OAuthRequest::from_request(); try { $server->verify_request($request); } catch (lti\OAuthException $e) { throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage()); } return $tool; } /** * Verifies the JWT signature using a JWK keyset. * * @param string $jwtparam JWT parameter value. * @param string $keyseturl The tool keyseturl. * @param string $clientid The tool client id. * * @return object The JWT's payload as a PHP object * @throws moodle_exception * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim */ function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) { // Attempts to retrieve cached keyset. $cache = cache::make('mod_lti', 'keyset'); $keyset = $cache->get($clientid); try { if (empty($keyset)) { throw new moodle_exception('errornocachedkeysetfound', 'mod_lti'); } $keysetarr = json_decode($keyset, true); $keys = JWK::parseKeySet($keysetarr); $jwt = JWT::decode($jwtparam, $keys); } catch (Exception $e) { // Something went wrong, so attempt to update cached keyset and then try again. $keyset = download_file_content($keyseturl); $keysetarr = json_decode($keyset, true); // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK. // The fix_jwks_alg() call only fixes a single, matched key and will leave others present (which may be missing alg too), // Remaining keys missing alg are excluded since they cannot be used for decoding anyway (no match to JWT kid). $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam); $keysetarr['keys'] = array_filter($keysetarr['keys'], fn($key) => isset($key['alg'])); // JWK::parseKeySet uses RS256 algorithm by default. $keys = JWK::parseKeySet($keysetarr); $jwt = JWT::decode($jwtparam, $keys); // If sucessful, updates the cached keyset. $cache->set($clientid, $keyset); } return $jwt; } /** * Verifies the JWT signature of an incoming message. * * @param int $typeid The tool type ID. * @param string $consumerkey The consumer key. * @param string $jwtparam JWT parameter value * * @return stdClass Tool type * @throws moodle_exception * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim */ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } if (isset($tool->toolproxyid)) { throw new moodle_exception('JWT security not supported with LTI 2'); } $typeconfig = lti_get_type_config($typeid); $key = $tool->clientid ?? ''; if ($consumerkey !== $key) { throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); } if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) { $publickey = $typeconfig['publickey'] ?? ''; if (empty($publickey)) { throw new moodle_exception('No public key configured'); } // Attemps to verify jwt with RSA key. JWT::decode($jwtparam, new Key($publickey, 'RS256')); } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) { $keyseturl = $typeconfig['publickeyset'] ?? ''; if (empty($keyseturl)) { throw new moodle_exception('No public keyset configured'); } // Attempts to verify jwt with jwk keyset. lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid); } else { throw new moodle_exception('Invalid public key type'); } return $tool; } /** * Converts an array of custom parameters to a new line separated string. * * @param object $params list of params to concatenate * * @return string */ function params_to_string(object $params) { $customparameters = []; foreach ($params as $key => $value) { $customparameters[] = "{$key}={$value}"; } return implode("\n", $customparameters); } /** * Converts LTI 1.1 Content Item for LTI Link to Form data. * * @param object $tool Tool for which the item is created for. * @param object $typeconfig The tool configuration. * @param object $item Item populated from JSON to be converted to Form form * * @return stdClass Form config for the item */ function content_item_to_form(object $tool, object $typeconfig, object $item): stdClass { global $OUTPUT; $config = new stdClass(); $config->name = ''; if (isset($item->title)) { $config->name = $item->title; } if (empty($config->name)) { $config->name = $tool->name; } if (isset($item->text)) { $config->introeditor = [ 'text' => $item->text, 'format' => FORMAT_PLAIN ]; } else { $config->introeditor = [ 'text' => '', 'format' => FORMAT_PLAIN ]; } if (isset($item->icon->{'@id'})) { $iconurl = new moodle_url($item->icon->{'@id'}); // Assign item's icon URL to secureicon or icon depending on its scheme. if (strtolower($iconurl->get_scheme()) === 'https') { $config->secureicon = $iconurl->out(false); } else { $config->icon = $iconurl->out(false); } } if (isset($item->url)) { $url = new moodle_url($item->url); $config->toolurl = $url->out(false); $config->typeid = 0; } else { $config->typeid = $tool->id; } $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER; $islti2 = $tool->ltiversion === LTI_VERSION_2; if (!$islti2 && isset($typeconfig->lti_acceptgrades)) { $acceptgrades = $typeconfig->lti_acceptgrades; if ($acceptgrades == LTI_SETTING_ALWAYS) { // We create a line item regardless if the definition contains one or not. $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; $config->grade_modgrade_point = 100; } if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) { if (isset($item->lineItem)) { $lineitem = $item->lineItem; $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; $maxscore = 100; if (isset($lineitem->scoreConstraints)) { $sc = $lineitem->scoreConstraints; if (isset($sc->totalMaximum)) { $maxscore = $sc->totalMaximum; } else if (isset($sc->normalMaximum)) { $maxscore = $sc->normalMaximum; } } $config->grade_modgrade_point = $maxscore; $config->lineitemresourceid = ''; $config->lineitemtag = ''; $config->lineitemsubreviewurl = ''; $config->lineitemsubreviewparams = ''; if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) { $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:''; } if (isset($lineitem->tag)) { $config->lineitemtag = $lineitem->tag?:''; } if (isset($lineitem->submissionReview)) { $subreview = $lineitem->submissionReview; $config->lineitemsubreviewurl = 'DEFAULT'; if (!empty($subreview->url)) { $config->lineitemsubreviewurl = $subreview->url; } if (isset($subreview->custom)) { $config->lineitemsubreviewparams = params_to_string($subreview->custom); } } } } } $config->instructorchoicesendname = LTI_SETTING_NEVER; $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER; // Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items. $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; if (isset($item->custom)) { $config->instructorcustomparameters = params_to_string($item->custom); } // Pass an indicator to the relevant form field. $config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti'); return $config; } /** * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the * selected content item. This configuration data can be then used when adding a tool into the course. * * @param int $typeid The tool type ID. * @param string $messagetype The value for the lti_message_type parameter. * @param string $ltiversion The value for the lti_version parameter. * @param string $consumerkey The consumer key. * @param string $contentitemsjson The JSON string for the content_items parameter. * @return stdClass The array of module information objects. * @throws moodle_exception * @throws lti\OAuthException */ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } // Check lti_message_type. Show debugging if it's not set to ContentItemSelection. // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment. if ($messagetype !== 'ContentItemSelection') { debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.", DEBUG_DEVELOPER); } // Check LTI versions from our side and the response's side. Show debugging if they don't match. // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment. $expectedversion = $tool->ltiversion; $islti2 = ($expectedversion === LTI_VERSION_2); if ($ltiversion !== $expectedversion) { debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," . " Response: {$ltiversion}", DEBUG_DEVELOPER); } $items = json_decode($contentitemsjson); if (empty($items)) { throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson); } if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) { throw new moodle_exception('errorinvalidresponseformat', 'mod_lti'); } $config = null; $items = $items->{'@graph'}; if (!empty($items)) { $typeconfig = lti_get_type_type_config($tool->id); if (count($items) == 1) { $config = content_item_to_form($tool, $typeconfig, $items[0]); } else { $multiple = []; foreach ($items as $item) { $multiple[] = content_item_to_form($tool, $typeconfig, $item); } $config = new stdClass(); $config->multiple = $multiple; } } return $config; } /** * Converts the new Deep-Linking format for Content-Items to the old format. * * @param string $param JSON string representing new Deep-Linking format * @return string JSON representation of content-items */ function lti_convert_content_items($param) { $items = array(); $json = json_decode($param); if (!empty($json) && is_array($json)) { foreach ($json as $item) { if (isset($item->type)) { $newitem = clone $item; switch ($item->type) { case 'ltiResourceLink': $newitem->{'@type'} = 'LtiLinkItem'; $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; break; case 'link': case 'rich': $newitem->{'@type'} = 'ContentItem'; $newitem->mediaType = 'text/html'; break; case 'file': $newitem->{'@type'} = 'FileItem'; break; } unset($newitem->type); if (isset($item->html)) { $newitem->text = $item->html; unset($newitem->html); } if (isset($item->iframe)) { // DeepLinking allows multiple options to be declared as supported. // We favor iframe over new window if both are specified. $newitem->placementAdvice = new stdClass(); $newitem->placementAdvice->presentationDocumentTarget = 'iframe'; if (isset($item->iframe->width)) { $newitem->placementAdvice->displayWidth = $item->iframe->width; } if (isset($item->iframe->height)) { $newitem->placementAdvice->displayHeight = $item->iframe->height; } unset($newitem->iframe); unset($newitem->window); } else if (isset($item->window)) { $newitem->placementAdvice = new stdClass(); $newitem->placementAdvice->presentationDocumentTarget = 'window'; if (isset($item->window->targetName)) { $newitem->placementAdvice->windowTarget = $item->window->targetName; } if (isset($item->window->width)) { $newitem->placementAdvice->displayWidth = $item->window->width; } if (isset($item->window->height)) { $newitem->placementAdvice->displayHeight = $item->window->height; } unset($newitem->window); } else if (isset($item->presentation)) { // This may have been part of an early draft but is not in the final spec // so keeping it around for now in case it's actually been used. $newitem->placementAdvice = new stdClass(); if (isset($item->presentation->documentTarget)) { $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget; } if (isset($item->presentation->windowTarget)) { $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget; } if (isset($item->presentation->width)) { $newitem->placementAdvice->dislayWidth = $item->presentation->width; } if (isset($item->presentation->height)) { $newitem->placementAdvice->dislayHeight = $item->presentation->height; } unset($newitem->presentation); } if (isset($item->icon) && isset($item->icon->url)) { $newitem->icon->{'@id'} = $item->icon->url; unset($newitem->icon->url); } if (isset($item->thumbnail) && isset($item->thumbnail->url)) { $newitem->thumbnail->{'@id'} = $item->thumbnail->url; unset($newitem->thumbnail->url); } if (isset($item->lineItem)) { unset($newitem->lineItem); $newitem->lineItem = new stdClass(); $newitem->lineItem->{'@type'} = 'LineItem'; $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore'; if (isset($item->lineItem->label)) { $newitem->lineItem->label = $item->lineItem->label; } if (isset($item->lineItem->resourceId)) { $newitem->lineItem->assignedActivity = new stdClass(); $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId; } if (isset($item->lineItem->tag)) { $newitem->lineItem->tag = $item->lineItem->tag; } if (isset($item->lineItem->scoreMaximum)) { $newitem->lineItem->scoreConstraints = new stdClass(); $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits'; $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum; } if (isset($item->lineItem->submissionReview)) { $newitem->lineItem->submissionReview = $item->lineItem->submissionReview; } } $items[] = $newitem; } } } $newitems = new stdClass(); $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; $newitems->{'@graph'} = $items; return json_encode($newitems); } function lti_get_tool_table($tools, $id) { global $OUTPUT; $html = ''; $typename = get_string('typename', 'lti'); $baseurl = get_string('baseurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); if (!empty($tools)) { $html .= "
"; foreach ($tools as $type) { $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); $accept = get_string('accept', 'lti'); $update = get_string('update', 'lti'); $delete = get_string('delete', 'lti'); if (empty($type->toolproxyid)) { $baseurl = new \moodle_url('/mod/lti/typessettings.php', array( 'action' => 'accept', 'id' => $type->id, 'sesskey' => sesskey(), 'tab' => $id )); $ref = $type->baseurl; } else { $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array( 'action' => 'accept', 'id' => $type->id, 'sesskey' => sesskey(), 'tab' => $id )); $ref = $type->tpname; } $accepthtml = $OUTPUT->action_icon($baseurl, new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, array('title' => $accept, 'class' => 'editing_accept')); $deleteaction = 'delete'; if ($type->state == LTI_TOOL_STATE_CONFIGURED) { $accepthtml = ''; } if ($type->state != LTI_TOOL_STATE_REJECTED) { $deleteaction = 'reject'; $delete = get_string('reject', 'lti'); } $updateurl = clone($baseurl); $updateurl->param('action', 'update'); $updatehtml = $OUTPUT->action_icon($updateurl, new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, array('title' => $update, 'class' => 'editing_update')); if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) { $deleteurl = clone($baseurl); $deleteurl->param('action', $deleteaction); $deletehtml = $OUTPUT->action_icon($deleteurl, new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, array('title' => $delete, 'class' => 'editing_delete')); } else { $deletehtml = ''; } $html .= " "; } $html .= '
$typename $baseurl $createdon $action
{$type->name} {$ref} {$date} {$accepthtml}{$updatehtml}{$deletehtml}
'; } else { $html .= get_string('no_' . $id, 'lti'); } return $html; } /** * This function builds the tab for a category of tool proxies * * @param object $toolproxies Tool proxy instance objects * @param string $id Category ID * * @return string HTML for tab */ function lti_get_tool_proxy_table($toolproxies, $id) { global $OUTPUT; if (!empty($toolproxies)) { $typename = get_string('typename', 'lti'); $url = get_string('registrationurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); $html = <<< EOD
EOD; foreach ($toolproxies as $toolproxy) { $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); $accept = get_string('register', 'lti'); $update = get_string('update', 'lti'); $delete = get_string('delete', 'lti'); $baseurl = new \moodle_url('/mod/lti/registersettings.php', array( 'action' => 'accept', 'id' => $toolproxy->id, 'sesskey' => sesskey(), 'tab' => $id )); $registerurl = new \moodle_url('/mod/lti/register.php', array( 'id' => $toolproxy->id, 'sesskey' => sesskey(), 'tab' => 'tool_proxy' )); $accepthtml = $OUTPUT->action_icon($registerurl, new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, array('title' => $accept, 'class' => 'editing_accept')); $deleteaction = 'delete'; if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) { $accepthtml = ''; } if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) { $delete = get_string('cancel', 'lti'); } $updateurl = clone($baseurl); $updateurl->param('action', 'update'); $updatehtml = $OUTPUT->action_icon($updateurl, new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, array('title' => $update, 'class' => 'editing_update')); $deleteurl = clone($baseurl); $deleteurl->param('action', $deleteaction); $deletehtml = $OUTPUT->action_icon($deleteurl, new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, array('title' => $delete, 'class' => 'editing_delete')); $html .= <<< EOD EOD; } $html .= '
{$typename} {$url} {$createdon} {$action}
{$toolproxy->name} {$toolproxy->regurl} {$date} {$accepthtml}{$updatehtml}{$deletehtml}
'; } else { $html = get_string('no_' . $id, 'lti'); } return $html; } /** * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter * * @param object $tool Tool instance object * * @return array List of enabled capabilities */ function lti_get_enabled_capabilities($tool) { if (!isset($tool)) { return array(); } if (!empty($tool->enabledcapability)) { $enabledcapabilities = explode("\n", $tool->enabledcapability); } else { $enabledcapabilities = array(); } if (!empty($tool->parameter)) { $paramstr = str_replace("\r\n", "\n", $tool->parameter); $paramstr = str_replace("\n\r", "\n", $paramstr); $paramstr = str_replace("\r", "\n", $paramstr); $params = explode("\n", $paramstr); foreach ($params as $param) { $pos = strpos($param, '='); if (($pos === false) || ($pos < 1)) { continue; } $value = trim(core_text::substr($param, $pos + 1, strlen($param))); if (substr($value, 0, 1) == '$') { $value = substr($value, 1); if (!in_array($value, $enabledcapabilities)) { $enabledcapabilities[] = $value; } } } } return $enabledcapabilities; } /** * Splits the custom parameters * * @param string $customstr String containing the parameters * * @return array of custom parameters */ function lti_split_parameters($customstr) { $customstr = str_replace("\r\n", "\n", $customstr); $customstr = str_replace("\n\r", "\n", $customstr); $customstr = str_replace("\r", "\n", $customstr); $lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"? $retval = array(); foreach ($lines as $line) { $pos = strpos($line, '='); if ( $pos === false || $pos < 1 ) { continue; } $key = trim(core_text::substr($line, 0, $pos)); $val = trim(core_text::substr($line, $pos + 1, strlen($line))); $retval[$key] = $val; } return $retval; } /** * Splits the custom parameters field to the various parameters * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param string $customstr String containing the parameters * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return array of custom parameters */ function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { $splitted = lti_split_parameters($customstr); $retval = array(); foreach ($splitted as $key => $val) { $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2); $key2 = lti_map_keyname($key); $retval['custom_'.$key2] = $val; if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) { $retval['custom_'.$key] = $val; } } return $retval; } /** * Adds the custom parameters to an array * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param array $parameters Array containing the parameters * * @return array Array of custom parameters */ function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) { $retval = array(); foreach ($parameters as $key => $val) { $key2 = lti_map_keyname($key); $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true); $retval['custom_'.$key2] = $val; if ($key != $key2) { $retval['custom_'.$key] = $val; } } return $retval; } /** * Parse a custom parameter to replace any substitution variables * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param string $value Custom parameter value * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return string Parsed value of custom parameter */ function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) { // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var. global $USER, $COURSE; if ($value) { if (substr($value, 0, 1) == '\\') { $value = substr($value, 1); } else if (substr($value, 0, 1) == '$') { $value1 = substr($value, 1); $enabledcapabilities = lti_get_enabled_capabilities($tool); if (!$islti2 || in_array($value1, $enabledcapabilities)) { $capabilities = lti_get_capabilities(); if (array_key_exists($value1, $capabilities)) { $val = $capabilities[$value1]; if ($val) { if (substr($val, 0, 1) != '$') { $value = $params[$val]; } else { $valarr = explode('->', substr($val, 1), 2); $value = "{${$valarr[0]}->{$valarr[1]}}"; $value = str_replace('
' , ' ', $value); $value = str_replace('
' , ' ', $value); $value = format_string($value); } } else { $value = lti_calculate_custom_parameter($value1); } } else { $val = $value; $services = lti_get_services(); foreach ($services as $service) { $service->set_tool_proxy($toolproxy); $service->set_type($tool); $value = $service->parse_value($val); if ($val != $value) { break; } } } } } } return $value; } /** * Calculates the value of a custom parameter that has not been specified earlier * * @param string $value Custom parameter value * * @return string Calculated value of custom parameter */ function lti_calculate_custom_parameter($value) { global $USER, $COURSE; switch ($value) { case 'Moodle.Person.userGroupIds': return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]); case 'Context.id.history': return implode(",", get_course_history($COURSE)); case 'CourseSection.timeFrame.begin': if (empty($COURSE->startdate)) { return ""; } $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC')); return $dt->format(DateTime::ATOM); case 'CourseSection.timeFrame.end': if (empty($COURSE->enddate)) { return ""; } $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC')); return $dt->format(DateTime::ATOM); } return null; } /** * Build the history chain for this course using the course originalcourseid. * * @param object $course course for which the history is returned. * * @return array ids of the source course in ancestry order, immediate parent 1st. */ function get_course_history($course) { global $DB; $history = []; $parentid = $course->originalcourseid; while (!empty($parentid) && !in_array($parentid, $history)) { $history[] = $parentid; $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid)); } return $history; } /** * Used for building the names of the different custom parameters * * @param string $key Parameter name * @param bool $tolower Do we want to convert the key into lower case? * @return string Processed name */ function lti_map_keyname($key, $tolower = true) { if ($tolower) { $newkey = ''; $key = core_text::strtolower(trim($key)); foreach (str_split($key) as $ch) { if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) { $newkey .= $ch; } else { $newkey .= '_'; } } } else { $newkey = $key; } return $newkey; } /** * Gets the IMS role string for the specified user and LTI course module. * * @param mixed $user User object or user id * @param int $cmid The course module id of the LTI activity * @param int $courseid The course id of the LTI activity * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return string A role string suitable for passing with an LTI launch */ function lti_get_ims_role($user, $cmid, $courseid, $islti2) { $roles = array(); if (empty($cmid)) { // If no cmid is passed, check if the user is a teacher in the course // This allows other modules to programmatically "fake" a launch without // a real LTI instance. $context = context_course::instance($courseid); if (has_capability('moodle/course:manageactivities', $context, $user)) { array_push($roles, 'Instructor'); } else { array_push($roles, 'Learner'); } } else { $context = context_module::instance($cmid); if (has_capability('mod/lti:manage', $context)) { array_push($roles, 'Instructor'); } else { array_push($roles, 'Learner'); } } if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) { // Make sure admins do not have the Learner role, then set admin role. $roles = array_diff($roles, array('Learner')); if (!$islti2) { array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator'); } else { array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator'); } } return join(',', $roles); } /** * Returns configuration details for the tool * * @param int $typeid Basic LTI tool typeid * * @return array Tool Configuration */ function lti_get_type_config($typeid) { global $DB; $query = "SELECT name, value FROM {lti_types_config} WHERE typeid = :typeid1 UNION ALL SELECT 'toolurl' AS name, baseurl AS value FROM {lti_types} WHERE id = :typeid2 UNION ALL SELECT 'icon' AS name, icon AS value FROM {lti_types} WHERE id = :typeid3 UNION ALL SELECT 'secureicon' AS name, secureicon AS value FROM {lti_types} WHERE id = :typeid4"; $typeconfig = array(); $configs = $DB->get_records_sql($query, array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid)); if (!empty($configs)) { foreach ($configs as $config) { $typeconfig[$config->name] = $config->value; } } return $typeconfig; } function lti_get_tools_by_url($url, $state, $courseid = null) { $domain = lti_get_domain_from_url($url); return lti_get_tools_by_domain($domain, $state, $courseid); } function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { global $DB, $SITE; $statefilter = ''; $coursefilter = ''; if ($state) { $statefilter = 'AND t.state = :state'; } if ($courseid && $courseid != $SITE->id) { $coursefilter = 'OR t.course = :courseid'; } $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]); $query = "SELECT t.* FROM {lti_types} t LEFT JOIN {lti_types_categories} tc on t.id = tc.typeid WHERE t.tooldomain = :tooldomain AND (t.course = :siteid $coursefilter) $statefilter AND (tc.id IS NULL OR tc.categoryid = :categoryid)"; return $DB->get_records_sql($query, [ 'courseid' => $courseid, 'siteid' => $SITE->id, 'tooldomain' => $domain, 'state' => $state, 'categoryid' => $coursecategory ]); } /** * Returns all basicLTI tools configured by the administrator * * @param int $course * * @return array */ function lti_filter_get_types($course) { global $DB; if (!empty($course)) { $where = "WHERE t.course = :course"; $params = array('course' => $course); } else { $where = ''; $params = array(); } $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id {$where}"; return $DB->get_records_sql($query, $params); } /** * Given an array of tools, filter them based on their state * * @param array $tools An array of lti_types records * @param int $state One of the LTI_TOOL_STATE_* constants * @return array */ function lti_filter_tool_types(array $tools, $state) { $return = array(); foreach ($tools as $key => $tool) { if ($tool->state == $state) { $return[$key] = $tool; } } return $return; } /** * Returns all lti types visible in this course * * @deprecated since Moodle 4.3 * @param int $courseid The id of the course to retieve types for * @param array $coursevisible options for 'coursevisible' field, * default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER] * @return stdClass[] All the lti types visible in the given course */ function lti_get_lti_types_by_course($courseid, $coursevisible = null) { debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.', DEBUG_DEVELOPER); global $USER; return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []); } /** * Returns tool types for lti add instance and edit page * * @return array Array of lti types */ function lti_get_types_for_add_instance() { global $COURSE, $USER; // Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances. $types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]]; $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id); foreach ($preconfiguredtypes as $type) { $types[$type->id] = $type; } return $types; } /** * Returns a list of configured types in the given course * * @param int $courseid The id of the course to retieve types for * @param int $sectionreturn section to return to for forming the URLs * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link */ function lti_get_configured_types($courseid, $sectionreturn = 0) { global $OUTPUT, $USER; $types = []; $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]); foreach ($preconfiguredtypes as $ltitype) { $type = new stdClass(); $type->id = $ltitype->id; $type->modclass = MOD_CLASS_ACTIVITY; $type->name = 'lti_type_' . $ltitype->id; // Clean the name. We don't want tags here. $type->title = clean_param($ltitype->name, PARAM_NOTAGS); $trimmeddescription = trim($ltitype->description ?? ''); if ($trimmeddescription != '') { // Clean the description. We don't want tags here. $type->help = clean_param($trimmeddescription, PARAM_NOTAGS); $type->helplink = get_string('modulename_shortcut_link', 'lti'); } $iconurl = get_tool_type_icon_url($ltitype); $iconclass = ''; if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) { // Do not filter the icon if it is not the default LTI activity icon. $iconclass = 'nofilter'; } $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]); $params = [ 'add' => 'lti', 'return' => 0, 'course' => $courseid, 'typeid' => $ltitype->id, ]; if (!is_null($sectionreturn)) { $params['sr'] = $sectionreturn; } $type->link = new moodle_url('/course/modedit.php', $params); $types[] = $type; } return $types; } function lti_get_domain_from_url($url) { $matches = array(); if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) { return $matches[1]; } } function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) { $possibletools = lti_get_tools_by_url($url, $state, $courseid); return lti_get_best_tool_by_url($url, $possibletools, $courseid); } function lti_get_url_thumbprint($url) { // Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later. if (preg_match('/https?:\/\//', $url) !== 1) { $url = 'http://'.$url; } $urlparts = parse_url(strtolower($url)); if (!isset($urlparts['path'])) { $urlparts['path'] = ''; } if (!isset($urlparts['query'])) { $urlparts['query'] = ''; } if (!isset($urlparts['host'])) { $urlparts['host'] = ''; } if (substr($urlparts['host'], 0, 4) === 'www.') { $urlparts['host'] = substr($urlparts['host'], 4); } $urllower = $urlparts['host'] . '/' . $urlparts['path']; if ($urlparts['query'] != '') { $urllower .= '?' . $urlparts['query']; } return $urllower; } function lti_get_best_tool_by_url($url, $tools, $courseid = null) { if (count($tools) === 0) { return null; } $urllower = lti_get_url_thumbprint($url); foreach ($tools as $tool) { $tool->_matchscore = 0; $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl); if ($urllower === $toolbaseurllower) { // 100 points for exact thumbprint match. $tool->_matchscore += 100; } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) { // 50 points if tool thumbprint starts with the base URL thumbprint. $tool->_matchscore += 50; } // Prefer course tools over site tools. if (!empty($courseid)) { // Minus 10 points for not matching the course id (global tools). if ($tool->course != $courseid) { $tool->_matchscore -= 10; } } } $bestmatch = array_reduce($tools, function($value, $tool) { if ($tool->_matchscore > $value->_matchscore) { return $tool; } else { return $value; } }, (object)array('_matchscore' => -1)); // None of the tools are suitable for this URL. if ($bestmatch->_matchscore <= 0) { return null; } return $bestmatch; } function lti_get_shared_secrets_by_key($key) { global $DB; // Look up the shared secret for the specified key in both the types_config table (for configured tools) // And in the lti resource table for ad-hoc tools. $lti13 = LTI_VERSION_1P3; $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value FROM {lti_types_config} t1 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid JOIN {lti_types} type ON t2.typeid = type.id WHERE t1.name = 'resourcekey' AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1 AND t2.name = 'password' AND type.state = :configured1 AND type.ltiversion <> :ltiversion UNION SELECT tp.secret AS value FROM {lti_tool_proxies} tp JOIN {lti_types} t ON tp.id = t.toolproxyid WHERE tp.guid = :key2 AND t.state = :configured2 UNION SELECT password AS value FROM {lti} WHERE resourcekey = :key3"; $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13, 'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key)); $values = array_map(function($item) { return $item->value; }, $sharedsecrets); // There should really only be one shared secret per key. But, we can't prevent // more than one getting entered. For instance, if the same key is used for two tool providers. return $values; } /** * Delete a Basic LTI configuration * * @param int $id Configuration id */ function lti_delete_type($id) { global $DB; // We should probably just copy the launch URL to the tool instances in this case... using a single query. /* $instances = $DB->get_records('lti', array('typeid' => $id)); foreach ($instances as $instance) { $instance->typeid = 0; $DB->update_record('lti', $instance); }*/ $DB->delete_records('lti_types', array('id' => $id)); $DB->delete_records('lti_types_config', array('typeid' => $id)); $DB->delete_records('lti_types_categories', array('typeid' => $id)); } function lti_set_state_for_type($id, $state) { global $DB; $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state)); } /** * Transforms a basic LTI object to an array * * @param object $ltiobject Basic LTI object * * @return array Basic LTI configuration details */ function lti_get_config($ltiobject) { $typeconfig = (array)$ltiobject; $additionalconfig = lti_get_type_config($ltiobject->typeid); $typeconfig = array_merge($typeconfig, $additionalconfig); return $typeconfig; } /** * * Generates some of the tool configuration based on the instance details * * @param int $id * * @return object configuration * */ function lti_get_type_config_from_instance($id) { global $DB; $instance = $DB->get_record('lti', array('id' => $id)); $config = lti_get_config($instance); $type = new \stdClass(); $type->lti_fix = $id; if (isset($config['toolurl'])) { $type->lti_toolurl = $config['toolurl']; } if (isset($config['instructorchoicesendname'])) { $type->lti_sendname = $config['instructorchoicesendname']; } if (isset($config['instructorchoicesendemailaddr'])) { $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr']; } if (isset($config['instructorchoiceacceptgrades'])) { $type->lti_acceptgrades = $config['instructorchoiceacceptgrades']; } if (isset($config['instructorchoiceallowroster'])) { $type->lti_allowroster = $config['instructorchoiceallowroster']; } if (isset($config['instructorcustomparameters'])) { $type->lti_allowsetting = $config['instructorcustomparameters']; } return $type; } /** * Generates some of the tool configuration based on the admin configuration details * * @param int $id * * @return stdClass Configuration details */ function lti_get_type_type_config($id) { global $DB; $basicltitype = $DB->get_record('lti_types', array('id' => $id)); $config = lti_get_type_config($id); $type = new \stdClass(); $type->lti_typename = $basicltitype->name; $type->typeid = $basicltitype->id; $type->course = $basicltitype->course; $type->toolproxyid = $basicltitype->toolproxyid; $type->lti_toolurl = $basicltitype->baseurl; $type->lti_ltiversion = $basicltitype->ltiversion; $type->lti_clientid = $basicltitype->clientid; $type->lti_clientid_disabled = $type->lti_clientid; $type->lti_description = $basicltitype->description; $type->lti_parameters = $basicltitype->parameter; $type->lti_icon = $basicltitype->icon; $type->lti_secureicon = $basicltitype->secureicon; if (isset($config['resourcekey'])) { $type->lti_resourcekey = $config['resourcekey']; } if (isset($config['password'])) { $type->lti_password = $config['password']; } if (isset($config['publickey'])) { $type->lti_publickey = $config['publickey']; } if (isset($config['publickeyset'])) { $type->lti_publickeyset = $config['publickeyset']; } if (isset($config['keytype'])) { $type->lti_keytype = $config['keytype']; } if (isset($config['initiatelogin'])) { $type->lti_initiatelogin = $config['initiatelogin']; } if (isset($config['redirectionuris'])) { $type->lti_redirectionuris = $config['redirectionuris']; } if (isset($config['sendname'])) { $type->lti_sendname = $config['sendname']; } if (isset($config['instructorchoicesendname'])) { $type->lti_instructorchoicesendname = $config['instructorchoicesendname']; } if (isset($config['sendemailaddr'])) { $type->lti_sendemailaddr = $config['sendemailaddr']; } if (isset($config['instructorchoicesendemailaddr'])) { $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr']; } if (isset($config['acceptgrades'])) { $type->lti_acceptgrades = $config['acceptgrades']; } if (isset($config['instructorchoiceacceptgrades'])) { $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades']; } if (isset($config['allowroster'])) { $type->lti_allowroster = $config['allowroster']; } if (isset($config['instructorchoiceallowroster'])) { $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster']; } if (isset($config['customparameters'])) { $type->lti_customparameters = $config['customparameters']; } if (isset($config['forcessl'])) { $type->lti_forcessl = $config['forcessl']; } if (isset($config['organizationid_default'])) { $type->lti_organizationid_default = $config['organizationid_default']; } else { // Tool was configured before this option was available and the default then was host. $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST; } if (isset($config['organizationid'])) { $type->lti_organizationid = $config['organizationid']; } if (isset($config['organizationurl'])) { $type->lti_organizationurl = $config['organizationurl']; } if (isset($config['organizationdescr'])) { $type->lti_organizationdescr = $config['organizationdescr']; } if (isset($config['launchcontainer'])) { $type->lti_launchcontainer = $config['launchcontainer']; } if (isset($config['coursevisible'])) { $type->lti_coursevisible = $config['coursevisible']; } if (isset($config['contentitem'])) { $type->lti_contentitem = $config['contentitem']; } if (isset($config['toolurl_ContentItemSelectionRequest'])) { $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest']; } if (isset($config['debuglaunch'])) { $type->lti_debuglaunch = $config['debuglaunch']; } if (isset($config['module_class_type'])) { $type->lti_module_class_type = $config['module_class_type']; } // Get the parameters from the LTI services. foreach ($config as $name => $value) { if (strpos($name, 'ltiservice_') === 0) { $type->{$name} = $config[$name]; } } return $type; } function lti_prepare_type_for_save($type, $config) { if (isset($config->lti_toolurl)) { $type->baseurl = $config->lti_toolurl; if (isset($config->lti_tooldomain)) { $type->tooldomain = $config->lti_tooldomain; } else { $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl); } } if (isset($config->lti_description)) { $type->description = $config->lti_description; } if (isset($config->lti_typename)) { $type->name = $config->lti_typename; } if (isset($config->lti_ltiversion)) { $type->ltiversion = $config->lti_ltiversion; } if (isset($config->lti_clientid)) { $type->clientid = $config->lti_clientid; } if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) { $type->clientid = registration_helper::get()->new_clientid(); } else if (empty($type->clientid)) { $type->clientid = null; } if (isset($config->lti_coursevisible)) { $type->coursevisible = $config->lti_coursevisible; } if (isset($config->lti_icon)) { $type->icon = $config->lti_icon; } if (isset($config->lti_secureicon)) { $type->secureicon = $config->lti_secureicon; } $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0; $config->lti_forcessl = $type->forcessl; if (isset($config->lti_contentitem)) { $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0; $config->lti_contentitem = $type->contentitem; } if (isset($config->lti_toolurl_ContentItemSelectionRequest)) { if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) { $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest; } else { $type->toolurl_ContentItemSelectionRequest = ''; } $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest; } $type->timemodified = time(); unset ($config->lti_typename); unset ($config->lti_toolurl); unset ($config->lti_description); unset ($config->lti_ltiversion); unset ($config->lti_clientid); unset ($config->lti_icon); unset ($config->lti_secureicon); } function lti_update_type($type, $config) { global $DB, $CFG; lti_prepare_type_for_save($type, $config); if (lti_request_is_using_ssl() && !empty($type->secureicon)) { $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon); } else { $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon)); } unset($config->oldicon); if ($DB->update_record('lti_types', $type)) { foreach ($config as $key => $value) { if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { $record = new \StdClass(); $record->typeid = $type->id; $record->name = substr($key, 4); $record->value = $value; lti_update_config($record); } if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) { $record = new \StdClass(); $record->typeid = $type->id; $record->name = $key; $record->value = $value; lti_update_config($record); } } if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) { // We need to remove the tool proxy for this tool to function under 1.3. $toolproxyid = $type->toolproxyid; $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid)); $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid)); $type->toolproxyid = null; $DB->update_record('lti_types', $type); } $DB->delete_records('lti_types_categories', ['typeid' => $type->id]); if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) { lti_type_add_categories($type->id, $config->lti_coursecategories); } require_once($CFG->libdir.'/modinfolib.php'); if ($clearcache) { $sql = "SELECT cm.id, cm.course FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id JOIN {lti} l ON l.course = cm.course WHERE m.name = :name AND l.typeid = :typeid"; $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]); $courseids = []; foreach ($rs as $record) { $courseids[] = $record->course; \course_modinfo::purge_course_module_cache($record->course, $record->id); } $rs->close(); $courseids = array_unique($courseids); foreach ($courseids as $courseid) { rebuild_course_cache($courseid, false, true); } } } } /** * Add LTI Type course category. * * @param int $typeid * @param string $lticoursecategories Comma separated list of course categories. * @return void */ function lti_type_add_categories(int $typeid, string $lticoursecategories = ''): void { global $DB; $coursecategories = explode(',', $lticoursecategories); foreach ($coursecategories as $coursecategory) { $DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]); } } function lti_add_type($type, $config) { global $USER, $SITE, $DB; lti_prepare_type_for_save($type, $config); if (!isset($type->state)) { $type->state = LTI_TOOL_STATE_PENDING; } if (!isset($type->ltiversion)) { $type->ltiversion = LTI_VERSION_1; } if (!isset($type->timecreated)) { $type->timecreated = time(); } if (!isset($type->createdby)) { $type->createdby = $USER->id; } if (!isset($type->course)) { $type->course = $SITE->id; } // Create a salt value to be used for signing passed data to extension services // The outcome service uses the service salt on the instance. This can be used // for communication with services not related to a specific LTI instance. $config->lti_servicesalt = uniqid('', true); $id = $DB->insert_record('lti_types', $type); if ($id) { foreach ($config as $key => $value) { if (!is_null($value)) { if (substr($key, 0, 4) === 'lti_') { $fieldname = substr($key, 4); } else if (substr($key, 0, 11) !== 'ltiservice_') { continue; } else { $fieldname = $key; } $record = new \StdClass(); $record->typeid = $id; $record->name = $fieldname; $record->value = $value; lti_add_config($record); } } if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) { lti_type_add_categories($id, $config->lti_coursecategories); } } return $id; } /** * Given an array of tool proxies, filter them based on their state * * @param array $toolproxies An array of lti_tool_proxies records * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants * * @return array */ function lti_filter_tool_proxy_types(array $toolproxies, $state) { $return = array(); foreach ($toolproxies as $key => $toolproxy) { if ($toolproxy->state == $state) { $return[$key] = $toolproxy; } } return $return; } /** * Get the tool proxy instance given its GUID * * @param string $toolproxyguid Tool proxy GUID value * * @return object */ function lti_get_tool_proxy_from_guid($toolproxyguid) { global $DB; $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid)); return $toolproxy; } /** * Get the tool proxy instance given its registration URL * * @param string $regurl Tool proxy registration URL * * @return array The record of the tool proxy with this url */ function lti_get_tool_proxies_from_registration_url($regurl) { global $DB; return $DB->get_records_sql( 'SELECT * FROM {lti_tool_proxies} WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl', array('regurl' => $regurl) ); } /** * Generates some of the tool proxy configuration based on the admin configuration details * * @param int $id * * @return mixed Tool Proxy details */ function lti_get_tool_proxy($id) { global $DB; $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id)); return $toolproxy; } /** * Returns lti tool proxies. * * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them * @return array of basicLTI types */ function lti_get_tool_proxies($orphanedonly) { global $DB; if ($orphanedonly) { $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL')); $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); foreach ($proxies as $key => $value) { if (in_array($value->id, $usedproxyids)) { unset($proxies[$key]); } } return $proxies; } else { return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); } } /** * Generates some of the tool proxy configuration based on the admin configuration details * * @param int $id * * @return mixed Tool Proxy details */ function lti_get_tool_proxy_config($id) { $toolproxy = lti_get_tool_proxy($id); $tp = new \stdClass(); $tp->lti_registrationname = $toolproxy->name; $tp->toolproxyid = $toolproxy->id; $tp->state = $toolproxy->state; $tp->lti_registrationurl = $toolproxy->regurl; $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered); $tp->lti_services = explode("\n", $toolproxy->serviceoffered); return $tp; } /** * Update the database with a tool proxy instance * * @param object $config Tool proxy definition * * @return int Record id number */ function lti_add_tool_proxy($config) { global $USER, $DB; $toolproxy = new \stdClass(); if (isset($config->lti_registrationname)) { $toolproxy->name = trim($config->lti_registrationname); } if (isset($config->lti_registrationurl)) { $toolproxy->regurl = trim($config->lti_registrationurl); } if (isset($config->lti_capabilities)) { $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities); } else { $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities())); } if (isset($config->lti_services)) { $toolproxy->serviceoffered = implode("\n", $config->lti_services); } else { $func = function($s) { return $s->get_id(); }; $servicenames = array_map($func, lti_get_services()); $toolproxy->serviceoffered = implode("\n", $servicenames); } if (isset($config->toolproxyid) && !empty($config->toolproxyid)) { $toolproxy->id = $config->toolproxyid; if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) { $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; $toolproxy->guid = random_string(); $toolproxy->secret = random_string(); } $id = lti_update_tool_proxy($toolproxy); } else { $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; $toolproxy->timemodified = time(); $toolproxy->timecreated = $toolproxy->timemodified; if (!isset($toolproxy->createdby)) { $toolproxy->createdby = $USER->id; } $toolproxy->guid = random_string(); $toolproxy->secret = random_string(); $id = $DB->insert_record('lti_tool_proxies', $toolproxy); } return $id; } /** * Updates a tool proxy in the database * * @param object $toolproxy Tool proxy * * @return int Record id number */ function lti_update_tool_proxy($toolproxy) { global $DB; $toolproxy->timemodified = time(); $id = $DB->update_record('lti_tool_proxies', $toolproxy); return $id; } /** * Delete a Tool Proxy * * @param int $id Tool Proxy id */ function lti_delete_tool_proxy($id) { global $DB; $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id)); $tools = $DB->get_records('lti_types', array('toolproxyid' => $id)); foreach ($tools as $tool) { lti_delete_type($tool->id); } $DB->delete_records('lti_tool_proxies', array('id' => $id)); } /** * Get both LTI tool proxies and tool types. * * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool * types. * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types * will be returned. * * @param int $limit Maximum number of tools returned. * @param int $offset Do not return tools before offset index. * @param bool $orphanedonly If true, only return orphaned proxies. * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id. * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types. */ function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array { global $DB; if ($orphanedonly) { $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false); $countsql = helper::get_tool_proxy_sql($orphanedonly, true); $proxies = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit); $totalproxiescount = $DB->count_records_sql($countsql); } else { $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC', '*', $offset, $limit); $totalproxiescount = $DB->count_records('lti_tool_proxies'); } // Find new offset and limit for tool types after getting proxies and set up query. $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative. $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative. $typesparams = []; if (!empty($toolproxyid)) { $typesparams['toolproxyid'] = $toolproxyid; } $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC', '*', $typesoffset, $typeslimit); return [$proxies, array_map('serialise_tool_type', $types)]; } /** * Get the total number of LTI tool types and tool proxies. * * @param bool $orphanedonly If true, only count orphaned proxies. * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id. * @return int Count of tools. */ function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int { global $DB; $typessql = "SELECT count(*) FROM {lti_types}"; $typesparams = []; if (!empty($toolproxyid)) { $typessql .= " WHERE toolproxyid = :toolproxyid"; $typesparams['toolproxyid'] = $toolproxyid; } $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true); $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause(); return $DB->count_records_sql($countsql, $typesparams); } /** * Add a tool configuration in the database * * @param object $config Tool configuration * * @return int Record id number */ function lti_add_config($config) { global $DB; return $DB->insert_record('lti_types_config', $config); } /** * Updates a tool configuration in the database * * @param object $config Tool configuration * * @return mixed Record id number */ function lti_update_config($config) { global $DB; $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name)); if ($old) { $config->id = $old->id; $return = $DB->update_record('lti_types_config', $config); } else { $return = $DB->insert_record('lti_types_config', $config); } return $return; } /** * Gets the tool settings * * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) * @param int $courseid Id of course (null if system settings) * @param int $instanceid Id of course module (null if system or context settings) * * @return array Array settings */ function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) { global $DB; $settings = array(); if ($toolproxyid > 0) { $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } else { $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } if ($settingsstr !== false) { $settings = json_decode($settingsstr, true); } return $settings; } /** * Sets the tool settings ( * * @param array $settings Array of settings * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) * @param int $courseid Id of course (null if system settings) * @param int $instanceid Id of course module (null if system or context settings) */ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) { global $DB; $json = json_encode($settings); if ($toolproxyid >= 0) { $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } else { $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } if ($record !== false) { $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); } else { $record = new \stdClass(); if ($toolproxyid > 0) { $record->toolproxyid = $toolproxyid; } else { $record->typeid = -$toolproxyid; } $record->course = $courseid; $record->coursemoduleid = $instanceid; $record->settings = $json; $record->timecreated = time(); $record->timemodified = $record->timecreated; $DB->insert_record('lti_tool_settings', $record); } } /** * Signs the petition to launch the external tool using OAuth * * @param array $oldparms Parameters to be passed for signing * @param string $endpoint url of the external tool * @param string $method Method for sending the parameters (e.g. POST) * @param string $oauthconsumerkey * @param string $oauthconsumersecret * @return array|null */ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { $parms = $oldparms; $testtoken = ''; // TODO: Switch to core oauthlib once implemented - MDL-30149. $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); $newparms = $accreq->get_parameters(); return $newparms; } /** * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT * * @param array $parms Parameters to be passed for signing * @param string $endpoint url of the external tool * @param string $oauthconsumerkey * @param string $typeid ID of LTI tool type * @param string $nonce Nonce value to use * @return array|null */ function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') { global $CFG; if (empty($typeid)) { $typeid = 0; } $messagetypemapping = lti_get_jwt_message_type_mapping(); if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) { $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']]; } if (isset($parms['roles'])) { $roles = explode(',', $parms['roles']); $newroles = array(); foreach ($roles as $role) { if (strpos($role, 'urn:lti:role:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21); } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25); } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24); } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) { $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}"; } $newroles[] = $role; } $parms['roles'] = implode(',', $newroles); } $now = time(); if (empty($nonce)) { $nonce = bin2hex(openssl_random_pseudo_bytes(10)); } $claimmapping = lti_get_jwt_claim_mapping(); $payload = array( 'nonce' => $nonce, 'iat' => $now, 'exp' => $now + 60, ); $payload['iss'] = $CFG->wwwroot; $payload['aud'] = $oauthconsumerkey; $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid); $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint; foreach ($parms as $key => $value) { $claim = LTI_JWT_CLAIM_PREFIX; if (array_key_exists($key, $claimmapping)) { $mapping = $claimmapping[$key]; $type = $mapping["type"] ?? "string"; if ($mapping['isarray']) { $value = explode(',', $value); sort($value); } else if ($type == 'boolean') { $value = isset($value) && ($value == 'true'); } if (!empty($mapping['suffix'])) { $claim .= "-{$mapping['suffix']}"; } $claim .= '/claim/'; if (is_null($mapping['group'])) { $payload[$mapping['claim']] = $value; } else if (empty($mapping['group'])) { $payload["{$claim}{$mapping['claim']}"] = $value; } else { $claim .= $mapping['group']; $payload[$claim][$mapping['claim']] = $value; } } else if (strpos($key, 'custom_') === 0) { $payload["{$claim}/claim/custom"][substr($key, 7)] = $value; } else if (strpos($key, 'ext_') === 0) { $payload["{$claim}/claim/ext"][substr($key, 4)] = $value; } } $privatekey = jwks_helper::get_private_key(); $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']); $newparms = array(); $newparms['id_token'] = $jwt; return $newparms; } /** * Verfies the JWT and converts its claims to their equivalent message parameter. * * @param int $typeid * @param string $jwtparam JWT parameter * * @return array message parameters * @throws moodle_exception */ function lti_convert_from_jwt($typeid, $jwtparam) { $params = array(); $parts = explode('.', $jwtparam); $ok = (count($parts) === 3); if ($ok) { $payload = JWT::urlsafeB64Decode($parts[1]); $claims = json_decode($payload, true); $ok = !is_null($claims) && !empty($claims['iss']); } if ($ok) { lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam); $params['oauth_consumer_key'] = $claims['iss']; foreach (lti_get_jwt_claim_mapping() as $key => $mapping) { $claim = LTI_JWT_CLAIM_PREFIX; if (!empty($mapping['suffix'])) { $claim .= "-{$mapping['suffix']}"; } $claim .= '/claim/'; if (is_null($mapping['group'])) { $claim = $mapping['claim']; } else if (empty($mapping['group'])) { $claim .= $mapping['claim']; } else { $claim .= $mapping['group']; } if (isset($claims[$claim])) { $value = null; if (empty($mapping['group'])) { $value = $claims[$claim]; } else { $group = $claims[$claim]; if (is_array($group) && array_key_exists($mapping['claim'], $group)) { $value = $group[$mapping['claim']]; } } if (!empty($value) && $mapping['isarray']) { if (is_array($value)) { if (is_array($value[0])) { $value = json_encode($value); } else { $value = implode(',', $value); } } } if (!is_null($value) && is_string($value) && (strlen($value) > 0)) { $params[$key] = $value; } } $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom'; if (isset($claims[$claim])) { $custom = $claims[$claim]; if (is_array($custom)) { foreach ($custom as $key => $value) { $params["custom_{$key}"] = $value; } } } $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext'; if (isset($claims[$claim])) { $ext = $claims[$claim]; if (is_array($ext)) { foreach ($ext as $key => $value) { $params["ext_{$key}"] = $value; } } } } } if (isset($params['content_items'])) { $params['content_items'] = lti_convert_content_items($params['content_items']); } $messagetypemapping = lti_get_jwt_message_type_mapping(); if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) { $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']]; } return $params; } /** * Posts the launch petition HTML * * @param array $newparms Signed parameters * @param string $endpoint URL of the external tool * @param bool $debug Debug (true/false) * @return string */ function lti_post_launch_html($newparms, $endpoint, $debug=false) { $r = "
\n"; // Contruct html for the launch parameters. foreach ($newparms as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); if ( $key == "ext_submit" ) { $r .= "\n"; } if ( $debug ) { $r .= "\n"; $r .= ""; $r .= get_string("toggle_debug_data", "lti")."\n"; $r .= "
\n"; $r .= "".get_string("basiclti_endpoint", "lti")."
\n"; $r .= $endpoint . "
\n 
\n"; $r .= "".get_string("basiclti_parameters", "lti")."
\n"; foreach ($newparms as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); $r .= "$key = $value
\n"; } $r .= " 
\n"; $r .= "
\n"; } $r .= "
\n"; // Auto-submit the form if endpoint is set. if ($endpoint !== '' && !$debug) { $r .= " \n"; } return $r; } /** * Generate the form for initiating a login request for an LTI 1.3 message * * @param int $courseid Course ID * @param int $cmid LTI instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type * @param string $title Title of content item * @param string $text Description of content item * @param int $foruserid Id of the user targeted by the launch * @return string */ function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '', $foruserid = 0) { global $SESSION; $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text); $r = "
lti_initiatelogin . "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " . "encType=\"application/x-www-form-urlencoded\">\n"; foreach ($params as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); $r .= " \n"; } $r .= "
\n"; $r .= "\n"; return $r; } /** * Prepares an LTI 1.3 login request * * @param int $courseid Course ID * @param int $cmid Course Module instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type * @param int $foruserid Id of the user targeted by the launch * @param string $title Title of content item * @param string $text Description of content item * @return array Login request parameters */ function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') { global $USER, $CFG, $SESSION; $ltihint = []; if (!empty($instance)) { $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl; $launchid = 'ltilaunch'.$instance->id.'_'.rand(); $ltihint['cmid'] = $cmid; $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,"; } else { $endpoint = $config->lti_toolurl; if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) { $endpoint = $config->lti_toolurl_ContentItemSelectionRequest; } $launchid = "ltilaunch_$messagetype".rand(); $SESSION->$launchid = "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text); } $endpoint = trim($endpoint); $services = lti_get_services(); foreach ($services as $service) { [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance); } $ltihint['launchid'] = $launchid; // If SSL is forced make sure https is on the normal launch URL. if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) { $endpoint = lti_ensure_url_is_https($endpoint); } else if (!strstr($endpoint, '://')) { $endpoint = 'http://' . $endpoint; } $params = array(); $params['iss'] = $CFG->wwwroot; $params['target_link_uri'] = $endpoint; $params['login_hint'] = $USER->id; $params['lti_message_hint'] = json_encode($ltihint); $params['client_id'] = $config->lti_clientid; $params['lti_deployment_id'] = $config->typeid; return $params; } function lti_get_type($typeid) { global $DB; return $DB->get_record('lti_types', array('id' => $typeid)); } function lti_get_launch_container($lti, $toolconfig) { if (empty($lti->launchcontainer)) { $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; } if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { if (isset($toolconfig['launchcontainer'])) { $launchcontainer = $toolconfig['launchcontainer']; } } else { $launchcontainer = $lti->launchcontainer; } if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; } $devicetype = core_useragent::get_device_type(); // Scrolling within the object element doesn't work on iOS or Android // Opening the popup window also had some issues in testing // For mobile devices, always take up the entire screen to ensure the best experience. if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) { $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW; } return $launchcontainer; } function lti_request_is_using_ssl() { global $CFG; return (stripos($CFG->wwwroot, 'https://') === 0); } function lti_ensure_url_is_https($url) { if (!strstr($url, '://')) { $url = 'https://' . $url; } else { // If the URL starts with http, replace with https. if (stripos($url, 'http://') === 0) { $url = 'https://' . substr($url, 7); } } return $url; } /** * Determines if we should try to log the request * * @param string $rawbody * @return bool */ function lti_should_log_request($rawbody) { global $CFG; if (empty($CFG->mod_lti_log_users)) { return false; } $logusers = explode(',', $CFG->mod_lti_log_users); if (empty($logusers)) { return false; } try { $xml = new \SimpleXMLElement($rawbody); $ns = $xml->getNamespaces(); $ns = array_shift($ns); $xml->registerXPathNamespace('lti', $ns); $requestuserid = ''; if ($node = $xml->xpath('//lti:userId')) { $node = $node[0]; $requestuserid = clean_param((string) $node, PARAM_INT); } else if ($node = $xml->xpath('//lti:sourcedId')) { $node = $node[0]; $resultjson = json_decode((string) $node); $requestuserid = clean_param($resultjson->data->userid, PARAM_INT); } } catch (Exception $e) { return false; } if (empty($requestuserid) or !in_array($requestuserid, $logusers)) { return false; } return true; } /** * Logs the request to a file in temp dir. * * @param string $rawbody */ function lti_log_request($rawbody) { if ($tempdir = make_temp_directory('mod_lti', false)) { if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) { $content = "Request Headers:\n"; foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) { $content .= "$header: $value\n"; } $content .= "Request Body:\n"; $content .= $rawbody; file_put_contents($tempfile, $content); chmod($tempfile, 0644); } } } /** * Log an LTI response. * * @param string $responsexml The response XML * @param Exception $e If there was an exception, pass that too */ function lti_log_response($responsexml, $e = null) { if ($tempdir = make_temp_directory('mod_lti', false)) { if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) { $content = ''; if ($e instanceof Exception) { $info = get_exception_info($e); $content .= "Exception:\n"; $content .= "Message: $info->message\n"; $content .= "Debug info: $info->debuginfo\n"; $content .= "Backtrace:\n"; $content .= format_backtrace($info->backtrace, true); $content .= "\n"; } $content .= "Response XML:\n"; $content .= $responsexml; file_put_contents($tempfile, $content); chmod($tempfile, 0644); } } } /** * Fetches LTI type configuration for an LTI instance * * @param stdClass $instance * @return array Can be empty if no type is found */ function lti_get_type_config_by_instance($instance) { $typeid = null; if (empty($instance->typeid)) { $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); if ($tool) { $typeid = $tool->id; } } else { $typeid = $instance->typeid; } if (!empty($typeid)) { return lti_get_type_config($typeid); } return array(); } /** * Enforce type config settings onto the LTI instance * * @param stdClass $instance * @param array $typeconfig */ function lti_force_type_config_settings($instance, array $typeconfig) { $forced = array( 'instructorchoicesendname' => 'sendname', 'instructorchoicesendemailaddr' => 'sendemailaddr', 'instructorchoiceacceptgrades' => 'acceptgrades', ); foreach ($forced as $instanceparam => $typeconfigparam) { if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) { $instance->$instanceparam = $typeconfig[$typeconfigparam]; } } } /** * Initializes an array with the capabilities supported by the LTI module * * @return array List of capability names (without a dollar sign prefix) */ function lti_get_capabilities() { $capabilities = array( 'basic-lti-launch-request' => '', 'ContentItemSelectionRequest' => '', 'ToolProxyRegistrationRequest' => '', 'Context.id' => 'context_id', 'Context.title' => 'context_title', 'Context.label' => 'context_label', 'Context.id.history' => null, 'Context.sourcedId' => 'lis_course_section_sourcedid', 'Context.longDescription' => '$COURSE->summary', 'Context.timeFrame.begin' => '$COURSE->startdate', 'CourseSection.title' => 'context_title', 'CourseSection.label' => 'context_label', 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 'CourseSection.longDescription' => '$COURSE->summary', 'CourseSection.timeFrame.begin' => null, 'CourseSection.timeFrame.end' => null, 'ResourceLink.id' => 'resource_link_id', 'ResourceLink.title' => 'resource_link_title', 'ResourceLink.description' => 'resource_link_description', 'User.id' => 'user_id', 'User.username' => '$USER->username', 'Person.name.full' => 'lis_person_name_full', 'Person.name.given' => 'lis_person_name_given', 'Person.name.family' => 'lis_person_name_family', 'Person.email.primary' => 'lis_person_contact_email_primary', 'Person.sourcedId' => 'lis_person_sourcedid', 'Person.name.middle' => '$USER->middlename', 'Person.address.street1' => '$USER->address', 'Person.address.locality' => '$USER->city', 'Person.address.country' => '$USER->country', 'Person.address.timezone' => '$USER->timezone', 'Person.phone.primary' => '$USER->phone1', 'Person.phone.mobile' => '$USER->phone2', 'Person.webaddress' => '$USER->url', 'Membership.role' => 'roles', 'Result.sourcedId' => 'lis_result_sourcedid', 'Result.autocreate' => 'lis_outcome_service_url', 'BasicOutcome.sourcedId' => 'lis_result_sourcedid', 'BasicOutcome.url' => 'lis_outcome_service_url', 'Moodle.Person.userGroupIds' => null); return $capabilities; } /** * Initializes an array with the services supported by the LTI module * * @return array List of services */ function lti_get_services() { $services = array(); $definedservices = core_component::get_plugin_list('ltiservice'); foreach ($definedservices as $name => $location) { $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; $services[] = new $classname(); } return $services; } /** * Initializes an instance of the named service * * @param string $servicename Name of service * * @return bool|\mod_lti\local\ltiservice\service_base Service */ function lti_get_service_by_name($servicename) { $service = false; $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}"; if (class_exists($classname)) { $service = new $classname(); } return $service; } /** * Finds a service by id * * @param \mod_lti\local\ltiservice\service_base[] $services Array of services * @param string $resourceid ID of resource * * @return mod_lti\local\ltiservice\service_base Service */ function lti_get_service_by_resource_id($services, $resourceid) { $service = false; foreach ($services as $aservice) { foreach ($aservice->get_resources() as $resource) { if ($resource->get_id() === $resourceid) { $service = $aservice; break 2; } } } return $service; } /** * Initializes an array with the scopes for services supported by the LTI module * and authorized for this particular tool instance. * * @param object $type LTI tool type * @param array $typeconfig LTI tool type configuration * * @return array List of scopes */ function lti_get_permitted_service_scopes($type, $typeconfig) { $services = lti_get_services(); $scopes = array(); foreach ($services as $service) { $service->set_type($type); $service->set_typeconfig($typeconfig); $servicescopes = $service->get_permitted_scopes(); if (!empty($servicescopes)) { $scopes = array_merge($scopes, $servicescopes); } } return $scopes; } /** * Extracts the named contexts from a tool proxy * * @param object $json * * @return array Contexts */ function lti_get_contexts($json) { $contexts = array(); if (isset($json->{'@context'})) { foreach ($json->{'@context'} as $context) { if (is_object($context)) { $contexts = array_merge(get_object_vars($context), $contexts); } } } return $contexts; } /** * Converts an ID to a fully-qualified ID * * @param array $contexts * @param string $id * * @return string Fully-qualified ID */ function lti_get_fqid($contexts, $id) { $parts = explode(':', $id, 2); if (count($parts) > 1) { if (array_key_exists($parts[0], $contexts)) { $id = $contexts[$parts[0]] . $parts[1]; } } return $id; } /** * Returns the icon for the given tool type * * @param stdClass $type The tool type * * @return string The url to the tool type's corresponding icon */ function get_tool_type_icon_url(stdClass $type) { global $OUTPUT; $iconurl = $type->secureicon; if (empty($iconurl)) { $iconurl = $type->icon; } if (empty($iconurl)) { $iconurl = $OUTPUT->image_url('monologo', 'lti')->out(); } return $iconurl; } /** * Returns the edit url for the given tool type * * @param stdClass $type The tool type * * @return string The url to edit the tool type */ function get_tool_type_edit_url(stdClass $type) { $url = new moodle_url('/mod/lti/typessettings.php', array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); return $url->out(); } /** * Returns the edit url for the given tool proxy. * * @param stdClass $proxy The tool proxy * * @return string The url to edit the tool type */ function get_tool_proxy_edit_url(stdClass $proxy) { $url = new moodle_url('/mod/lti/registersettings.php', array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); return $url->out(); } /** * Returns the course url for the given tool type * * @param stdClass $type The tool type * * @return string The url to the course of the tool type, void if it is a site wide type */ function get_tool_type_course_url(stdClass $type) { if ($type->course != 1) { $url = new moodle_url('/course/view.php', array('id' => $type->course)); return $url->out(); } return null; } /** * Returns the icon and edit urls for the tool type and the course url if it is a course type. * * @param stdClass $type The tool type * * @return array The urls of the tool type */ function get_tool_type_urls(stdClass $type) { $courseurl = get_tool_type_course_url($type); $urls = array( 'icon' => get_tool_type_icon_url($type), 'edit' => get_tool_type_edit_url($type), ); if ($courseurl) { $urls['course'] = $courseurl; } $url = new moodle_url('/mod/lti/certs.php'); $urls['publickeyset'] = $url->out(); $url = new moodle_url('/mod/lti/token.php'); $urls['accesstoken'] = $url->out(); $url = new moodle_url('/mod/lti/auth.php'); $urls['authrequest'] = $url->out(); return $urls; } /** * Returns the icon and edit urls for the tool proxy. * * @param stdClass $proxy The tool proxy * * @return array The urls of the tool proxy */ function get_tool_proxy_urls(stdClass $proxy) { global $OUTPUT; $urls = array( 'icon' => $OUTPUT->image_url('monologo', 'lti')->out(), 'edit' => get_tool_proxy_edit_url($proxy), ); return $urls; } /** * Returns information on the current state of the tool type * * @param stdClass $type The tool type * * @return array An array with a text description of the state, and boolean for whether it is in each state: * pending, configured, rejected, unknown */ function get_tool_type_state_info(stdClass $type) { $isconfigured = false; $ispending = false; $isrejected = false; $isunknown = false; switch ($type->state) { case LTI_TOOL_STATE_CONFIGURED: $state = get_string('active', 'mod_lti'); $isconfigured = true; break; case LTI_TOOL_STATE_PENDING: $state = get_string('pending', 'mod_lti'); $ispending = true; break; case LTI_TOOL_STATE_REJECTED: $state = get_string('rejected', 'mod_lti'); $isrejected = true; break; default: $state = get_string('unknownstate', 'mod_lti'); $isunknown = true; break; } return array( 'text' => $state, 'pending' => $ispending, 'configured' => $isconfigured, 'rejected' => $isrejected, 'unknown' => $isunknown ); } /** * Returns information on the configuration of the tool type * * @param stdClass $type The tool type * * @return array An array with configuration details */ function get_tool_type_config($type) { global $CFG; $platformid = $CFG->wwwroot; $clientid = $type->clientid; $deploymentid = $type->id; $publickeyseturl = new moodle_url('/mod/lti/certs.php'); $publickeyseturl = $publickeyseturl->out(); $accesstokenurl = new moodle_url('/mod/lti/token.php'); $accesstokenurl = $accesstokenurl->out(); $authrequesturl = new moodle_url('/mod/lti/auth.php'); $authrequesturl = $authrequesturl->out(); return array( 'platformid' => $platformid, 'clientid' => $clientid, 'deploymentid' => $deploymentid, 'publickeyseturl' => $publickeyseturl, 'accesstokenurl' => $accesstokenurl, 'authrequesturl' => $authrequesturl ); } /** * Returns a summary of each LTI capability this tool type requires in plain language * * @param stdClass $type The tool type * * @return array An array of text descriptions of each of the capabilities this tool type requires */ function get_tool_type_capability_groups($type) { $capabilities = lti_get_enabled_capabilities($type); $groups = array(); $hascourse = false; $hasactivities = false; $hasuseraccount = false; $hasuserpersonal = false; foreach ($capabilities as $capability) { // Bail out early if we've already found all groups. if (count($groups) >= 4) { continue; } if (!$hascourse && preg_match('/^CourseSection/', $capability)) { $hascourse = true; $groups[] = get_string('courseinformation', 'mod_lti'); } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) { $hasactivities = true; $groups[] = get_string('courseactivitiesorresources', 'mod_lti'); } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) { $hasuseraccount = true; $groups[] = get_string('useraccountinformation', 'mod_lti'); } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) { $hasuserpersonal = true; $groups[] = get_string('userpersonalinformation', 'mod_lti'); } } return $groups; } /** * Returns the ids of each instance of this tool type * * @param stdClass $type The tool type * * @return array An array of ids of the instances of this tool type */ function get_tool_type_instance_ids($type) { global $DB; return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id))); } /** * Serialises this tool type * * @param stdClass $type The tool type * * @return array An array of values representing this type */ function serialise_tool_type(stdClass $type) { global $CFG; $capabilitygroups = get_tool_type_capability_groups($type); $instanceids = get_tool_type_instance_ids($type); // Clean the name. We don't want tags here. $name = clean_param($type->name, PARAM_NOTAGS); if (!empty($type->description)) { // Clean the description. We don't want tags here. $description = clean_param($type->description, PARAM_NOTAGS); } else { $description = get_string('editdescription', 'mod_lti'); } return array( 'id' => $type->id, 'name' => $name, 'description' => $description, 'urls' => get_tool_type_urls($type), 'state' => get_tool_type_state_info($type), 'platformid' => $CFG->wwwroot, 'clientid' => $type->clientid, 'deploymentid' => $type->id, 'hascapabilitygroups' => !empty($capabilitygroups), 'capabilitygroups' => $capabilitygroups, // Course ID of 1 means it's not linked to a course. 'courseid' => $type->course == 1 ? 0 : $type->course, 'instanceids' => $instanceids, 'instancecount' => count($instanceids) ); } /** * Loads the cartridge information into the tool type, if the launch url is for a cartridge file * * @param stdClass $type The tool type object to be filled in * @since Moodle 3.1 */ function lti_load_type_if_cartridge($type) { if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) { lti_load_type_from_cartridge($type->lti_toolurl, $type); } } /** * Loads the cartridge information into the new tool, if the launch url is for a cartridge file * * @param stdClass $lti The tools config * @since Moodle 3.1 */ function lti_load_tool_if_cartridge($lti) { if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) { lti_load_tool_from_cartridge($lti->toolurl, $lti); } } /** * Determines if the given url is for a IMS basic cartridge * * @param string $url The url to be checked * @return True if the url is for a cartridge * @since Moodle 3.1 */ function lti_is_cartridge($url) { // If it is empty, it's not a cartridge. if (empty($url)) { return false; } // If it has xml at the end of the url, it's a cartridge. if (preg_match('/\.xml$/', $url)) { return true; } // Skip slow cartridge checks during tests. // During tests, working .xml cartridge URLs are used when testing cartridge support. These will match the '.xml' check // above (which is fast). Don't try to check whether other tool URLs are cartridges because most URLs used in tests will be // example URLs and won't be resolvable, resulting in network hangs within load_cartridge() - which is called every time a // tool is edited and will result in slow tests or seemingly random test failures. if (!defined('BEHAT_SITE_RUNNING') && !defined('PHPUNIT_TEST')) { // Even if it doesn't have .xml, load the url to check if it's a cartridge.. try { $toolinfo = lti_load_cartridge($url, array( "launch_url" => "launchurl" ) ); if (!empty($toolinfo['launchurl'])) { return true; } } catch (moodle_exception $e) { return false; // Error loading the xml, so it's not a cartridge. } } return false; } /** * Allows you to load settings for an external tool type from an IMS cartridge. * * @param string $url The URL to the cartridge * @param stdClass $type The tool type object to be filled in * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_type_from_cartridge($url, $type) { $toolinfo = lti_load_cartridge($url, array( "title" => "lti_typename", "launch_url" => "lti_toolurl", "description" => "lti_description", "icon" => "lti_icon", "secure_icon" => "lti_secureicon" ), array( "icon_url" => "lti_extension_icon", "secure_icon_url" => "lti_extension_secureicon" ) ); // If an activity name exists, unset the cartridge name so we don't override it. if (isset($type->lti_typename)) { unset($toolinfo['lti_typename']); } // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) { $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon']; } unset($toolinfo['lti_extension_icon']); if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) { $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon']; } unset($toolinfo['lti_extension_secureicon']); // Ensure Custom icons aren't overridden by cartridge params. if (!empty($type->lti_icon)) { unset($toolinfo['lti_icon']); } if (!empty($type->lti_secureicon)) { unset($toolinfo['lti_secureicon']); } foreach ($toolinfo as $property => $value) { $type->$property = $value; } } /** * Allows you to load in the configuration for an external tool from an IMS cartridge. * * @param string $url The URL to the cartridge * @param stdClass $lti LTI object * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_tool_from_cartridge($url, $lti) { $toolinfo = lti_load_cartridge($url, array( "title" => "name", "launch_url" => "toolurl", "secure_launch_url" => "securetoolurl", "description" => "intro", "icon" => "icon", "secure_icon" => "secureicon" ), array( "icon_url" => "extension_icon", "secure_icon_url" => "extension_secureicon" ) ); // If an activity name exists, unset the cartridge name so we don't override it. if (isset($lti->name)) { unset($toolinfo['name']); } // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) { $toolinfo['icon'] = $toolinfo['extension_icon']; } unset($toolinfo['extension_icon']); if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) { $toolinfo['secureicon'] = $toolinfo['extension_secureicon']; } unset($toolinfo['extension_secureicon']); foreach ($toolinfo as $property => $value) { $lti->$property = $value; } } /** * Search for a tag within an XML DOMDocument * * @param string $url The url of the cartridge to be loaded * @param array $map The map of tags to keys in the return array * @param array $propertiesmap The map of properties to keys in the return array * @return array An associative array with the given keys and their values from the cartridge * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_cartridge($url, $map, $propertiesmap = array()) { global $CFG; require_once($CFG->libdir. "/filelib.php"); $curl = new curl(); $response = $curl->get($url); // Got a completely empty response (real or error), cannot process this with // DOMDocument::loadXML() because it errors with ValueError. So let's throw // the moodle_exception before waiting to examine the errors later. if (trim($response) === '') { throw new moodle_exception('errorreadingfile', '', '', $url); } // TODO MDL-46023 Replace this code with a call to the new library. $origerrors = libxml_use_internal_errors(true); libxml_clear_errors(); $document = new DOMDocument(); @$document->loadXML($response, LIBXML_NONET); $cartridge = new DomXpath($document); $errors = libxml_get_errors(); libxml_clear_errors(); libxml_use_internal_errors($origerrors); if (count($errors) > 0) { $message = 'Failed to load cartridge.'; foreach ($errors as $error) { $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line; } throw new moodle_exception('errorreadingfile', '', '', $url, $message); } $toolinfo = array(); foreach ($map as $tag => $key) { $value = get_tag($tag, $cartridge); if ($value) { $toolinfo[$key] = $value; } } if (!empty($propertiesmap)) { foreach ($propertiesmap as $property => $key) { $value = get_tag("property", $cartridge, $property); if ($value) { $toolinfo[$key] = $value; } } } return $toolinfo; } /** * Search for a tag within an XML DOMDocument * * @param stdClass $tagname The name of the tag to search for * @param XPath $xpath The XML to find the tag in * @param XPath $attribute The attribute to search for (if we should search for a child node with the given * value for the name attribute * @since Moodle 3.1 */ function get_tag($tagname, $xpath, $attribute = null) { if ($attribute) { $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]'); } else { $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']'); } if ($result->length > 0) { return $result->item(0)->nodeValue; } return null; } /** * Create a new access token. * * @param int $typeid Tool type ID * @param string[] $scopes Scopes permitted for new token * * @return stdClass Access token */ function lti_new_access_token($typeid, $scopes) { global $DB; // Make sure the token doesn't exist (even if it should be almost impossible with the random generation). $numtries = 0; do { $numtries ++; $generatedtoken = md5(uniqid(rand(), 1)); if ($numtries > 5) { throw new moodle_exception('Failed to generate LTI access token'); } } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken))); $newtoken = new stdClass(); $newtoken->typeid = $typeid; $newtoken->scope = json_encode(array_values($scopes)); $newtoken->token = $generatedtoken; $newtoken->timecreated = time(); $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE; $newtoken->lastaccess = null; $DB->insert_record('lti_access_tokens', $newtoken); return $newtoken; } TrivialStore.php000064400000011575152265520610007721 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains a Trivial memory-based store - no support for tokens * * @package mod_lti * @copyright IMS Global Learning Consortium * * @author Charles Severance csev@umich.edu * * @license http://www.apache.org/licenses/LICENSE-2.0 */ namespace moodle\mod\lti; // Using a namespace as the basicLTI module imports classes with the same names. defined('MOODLE_INTERNAL') || die; /** * A Trivial memory-based store - no support for tokens. */ class TrivialOAuthDataStore extends OAuthDataStore { /** @var array $consumers Array of tool consumer keys and secrets */ private $consumers = array(); /** * Add a consumer to the array * * @param string $consumerkey Consumer key * @param string $consumersecret Consumer secret */ public function add_consumer($consumerkey, $consumersecret) { $this->consumers[$consumerkey] = $consumersecret; } /** * Get OAuth consumer given its key * * @param string $consumerkey Consumer key * * @return moodle\mod\lti\OAuthConsumer OAuthConsumer object */ public function lookup_consumer($consumerkey) { if (strpos($consumerkey, "http://" ) === 0) { $consumer = new OAuthConsumer($consumerkey, "secret", null); return $consumer; } if ( $this->consumers[$consumerkey] ) { $consumer = new OAuthConsumer($consumerkey, $this->consumers[$consumerkey], null); return $consumer; } return null; } /** * Create a dummy OAuthToken object for a consumer * * @param moodle\mod\lti\OAuthConsumer $consumer Consumer * @param string $tokentype Type of token * @param string $token Token ID * * @return moodle\mod\lti\OAuthToken OAuthToken object */ public function lookup_token($consumer, $tokentype, $token) { return new OAuthToken($consumer, ''); } /** * Nonce values are not checked so just return a null * * @param moodle\mod\lti\OAuthConsumer $consumer Consumer * @param string $token Token ID * @param string $nonce Nonce value * @param string $timestamp Timestamp * * @return null */ public function lookup_nonce($consumer, $token, $nonce, $timestamp) { // Should add some clever logic to keep nonces from // being reused - for now we are really trusting // that the timestamp will save us. return null; } /** * Tokens are not used so just return a null. * * @param moodle\mod\lti\OAuthConsumer $consumer Consumer * * @return null */ public function new_request_token($consumer) { return null; } /** * Tokens are not used so just return a null. * * @param string $token Token ID * @param moodle\mod\lti\OAuthConsumer $consumer Consumer * * @return null */ public function new_access_token($token, $consumer) { return null; } } toolssettings.php000064400000007453152265520610010213 0ustar00. /** * This file contains the script used to register a new external tool. * * It is used to create a new form used to configure the capabilities * and services to be offered to the tool provider. * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/edit_form.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $action = optional_param('action', '', PARAM_ALPHANUMEXT); $id = optional_param('id', '', PARAM_INT); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $returnto = optional_param('returnto', '', PARAM_ALPHA); if ($returnto == 'toolconfigure') { $returnurl = new moodle_url($CFG->wwwroot . '/mod/lti/toolconfigure.php'); } // No guest autologin. require_login(0, false); require_sesskey(); // Check this is for a tool created from a tool proxy. $err = empty($id); if (!$err) { $type = lti_get_type_type_config($id); $err = empty($type->toolproxyid); } if ($err) { $params = array('action' => $action, 'id' => $id, 'sesskey' => sesskey(), 'tab' => $tab); if (!empty($returnto)) { $params['returnto'] = $returnto; } $redirect = new moodle_url('/mod/lti/typessettings.php', $params); redirect($redirect); } $pageurl = new moodle_url('/mod/lti/toolssettings.php'); if (!empty($id)) { $pageurl->param('id', $id); } if (!empty($returnto)) { $pageurl->param('returnto', $returnto); } $PAGE->set_url($pageurl); admin_externalpage_setup('managemodules'); // Hacky solution for printing the admin page. $redirect = "$CFG->wwwroot/$CFG->admin/settings.php?section=modsettinglti&tab={$tab}"; if (!empty($returnurl)) { $redirect = $returnurl; } if ($action == 'accept') { lti_set_state_for_type($id, LTI_TOOL_STATE_CONFIGURED); redirect($redirect); } else if (($action == 'reject') || ($action == 'delete')) { lti_set_state_for_type($id, LTI_TOOL_STATE_REJECTED); redirect($redirect); } if (lti_request_is_using_ssl() && !empty($type->lti_secureicon)) { $type->oldicon = $type->lti_secureicon; } else { $type->oldicon = $type->lti_icon; } $form = new mod_lti_edit_types_form($pageurl, (object)array('isadmin' => true, 'istool' => true)); if ($data = $form->get_data()) { $type = new stdClass(); if (!empty($id)) { $type->id = $id; lti_update_type($type, $data); } else { $type->state = LTI_TOOL_STATE_CONFIGURED; lti_add_type($type, $data); } redirect($redirect); } else if ($form->is_cancelled()) { redirect($redirect); } $PAGE->set_title(get_string('toolsetup', 'lti')); $PAGE->navbar->add(get_string('lti_administration', 'lti'), $CFG->wwwroot.'/'.$CFG->admin.'/settings.php?section=modsettinglti'); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('toolsetup', 'lti')); echo $OUTPUT->box_start('generalbox'); if ($action == 'update') { $form->set_data($type); } $form->display(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); typessettings.php000064400000014417152265520610010215 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains the script used to clone Moodle admin setting page. * * It is used to create a new form used to pre-configure lti activities * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/edit_form.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $action = optional_param('action', null, PARAM_ALPHANUMEXT); $id = optional_param('id', null, PARAM_INT); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $returnto = optional_param('returnto', '', PARAM_ALPHA); if ($returnto == 'toolconfigure') { $returnurl = new moodle_url($CFG->wwwroot . '/mod/lti/toolconfigure.php'); } // No guest autologin. require_login(0, false); require_sesskey(); // Check this is not for a tool created from a tool proxy. if (!empty($id)) { $type = lti_get_type_type_config($id); if (!empty($type->toolproxyid)) { $sesskey = required_param('sesskey', PARAM_RAW); $params = array('action' => $action, 'id' => $id, 'sesskey' => $sesskey, 'tab' => $tab); if (!empty($returnto)) { $params['returnto'] = $returnto; } $redirect = new moodle_url('/mod/lti/toolssettings.php', $params); redirect($redirect); } $type->lti_coursecategories = ''; if ($records = $DB->get_fieldset_select('lti_types_categories', 'categoryid', 'typeid = :typeid', ['typeid' => $id])) { $type->lti_coursecategories = implode(',', $records); } } else { $type = new stdClass(); // Assign a default empty value for the lti_icon. $type->lti_icon = ''; $type->lti_secureicon = ''; $type->lti_clientid = null; $type->lti_coursecategories = ''; } $pageurl = new moodle_url('/mod/lti/typessettings.php'); if (!empty($id)) { $pageurl->param('id', $id); } if (!empty($returnto)) { $pageurl->param('returnto', $returnto); } $PAGE->set_url($pageurl); admin_externalpage_setup('managemodules'); // Hacky solution for printing the admin page. $redirect = "$CFG->wwwroot/$CFG->admin/settings.php?section=modsettinglti&tab={$tab}"; if (!empty($returnurl)) { $redirect = $returnurl; } if ($action == 'accept') { lti_set_state_for_type($id, LTI_TOOL_STATE_CONFIGURED); redirect($redirect); } else if ($action == 'reject') { lti_set_state_for_type($id, LTI_TOOL_STATE_REJECTED); redirect($redirect); } else if ($action == 'delete') { lti_delete_type($id); redirect($redirect); } if (lti_request_is_using_ssl() && !empty($type->lti_secureicon)) { $type->oldicon = $type->lti_secureicon; } else { $type->oldicon = $type->lti_icon; } $form = new mod_lti_edit_types_form( $pageurl, (object) [ 'isadmin' => true, 'istool' => false, 'id' => $id, 'clientid' => $type->lti_clientid, 'coursecategories' => $type->lti_coursecategories, 'iscoursetool' => !empty($id) && $type->course !== get_site()->id ] ); if ($data = $form->get_data()) { $type = new stdClass(); if (!empty($id)) { $type->id = $id; lti_load_type_if_cartridge($data); lti_update_type($type, $data); redirect($redirect); } else { $type->state = LTI_TOOL_STATE_CONFIGURED; lti_load_type_if_cartridge($data); lti_add_type($type, $data); redirect($redirect); } } else if ($form->is_cancelled()) { redirect($redirect); } $PAGE->set_title(get_string('toolsetup', 'lti')); $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->set_secondary_active_tab('ltitoolconfigure'); $PAGE->navbar->add(get_string('manage_external_tools', 'lti'), new moodle_url('/mod/lti/toolconfigure.php')); $PAGE->navbar->add(get_string('toolsetup', 'lti'), $PAGE->url); $PAGE->requires->js_call_amd('mod_lti/coursecategory', 'init', [$type->lti_coursecategories]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('toolsetup', 'lti')); echo $OUTPUT->box_start('generalbox'); if ($action == 'update') { $form->set_data($type); } $form->display(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); contentitem_return.php000064400000007402152265520610011214 0ustar00. /** * Handle the return from the Tool Provider after selecting a content item. * * @package mod_lti * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $id = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $jwt = optional_param('JWT', '', PARAM_RAW); $context = context_course::instance($courseid); $pageurl = new moodle_url('/mod/lti/contentitem_return.php'); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('popup'); $PAGE->set_context($context); // Cross-Site causes the cookie to be lost if not POSTed from same site. global $_POST; if (!empty($_POST["repost"])) { // Unset the param so that LTI 1.1 signature validation passes. unset($_POST["repost"]); } else if (!isloggedin()) { header_remove("Set-Cookie"); $output = $PAGE->get_renderer('mod_lti'); $page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST); echo $output->header(); echo $output->render($page); echo $output->footer(); return; } if (!empty($jwt)) { $params = lti_convert_from_jwt($id, $jwt); $consumerkey = $params['oauth_consumer_key'] ?? ''; $messagetype = $params['lti_message_type'] ?? ''; $version = $params['lti_version'] ?? ''; $items = $params['content_items'] ?? ''; $errormsg = $params['lti_errormsg'] ?? ''; $msg = $params['lti_msg'] ?? ''; } else { $consumerkey = required_param('oauth_consumer_key', PARAM_RAW); $messagetype = required_param('lti_message_type', PARAM_TEXT); $version = required_param('lti_version', PARAM_TEXT); $items = optional_param('content_items', '', PARAM_RAW); $errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); $msg = optional_param('lti_msg', '', PARAM_TEXT); lti_verify_oauth_signature($id, $consumerkey); } $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); require_login($course); require_sesskey(); require_capability('moodle/course:manageactivities', $context); require_capability('mod/lti:addcoursetool', $context); $redirecturl = null; $returndata = null; if (empty($errormsg) && !empty($items)) { try { $returndata = lti_tool_configuration_from_content_item($id, $messagetype, $version, $consumerkey, $items); } catch (moodle_exception $e) { $errormsg = $e->getMessage(); } } echo $OUTPUT->header(); // Call JS module to redirect the user to the course page or close the dialogue on error/cancel. $PAGE->requires->js_call_amd('mod_lti/contentitem_return', 'init', [$returndata]); echo $OUTPUT->footer(); // Add messages to notification stack for rendering later. if ($errormsg) { // Content item selection has encountered an error. \core\notification::error($errormsg); } else if (!empty($returndata)) { // Means success. if (!$msg) { $msg = get_string('successfullyfetchedtoolconfigurationfromcontent', 'lti'); } \core\notification::success($msg); } index.php000064400000010753152265520610006376 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This page lists all the instances of lti in a particular course * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/lti/lib.php'); $id = required_param('id', PARAM_INT); // Course id. $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); require_login($course); $PAGE->set_pagelayout('incourse'); $params = array( 'context' => context_course::instance($course->id) ); $event = \mod_lti\event\course_module_instance_list_viewed::create($params); $event->add_record_snapshot('course', $course); $event->trigger(); $PAGE->set_url('/mod/lti/index.php', array('id' => $course->id)); $pagetitle = strip_tags($course->shortname.': '.get_string("modulenamepluralformatted", "lti")); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); // Print the main part of the page. echo $OUTPUT->heading(get_string("modulenamepluralformatted", "lti")); // Get all the appropriate data. if (! $basicltis = get_all_instances_in_course("lti", $course)) { notice(get_string('noltis', 'lti'), "../../course/view.php?id=$course->id"); die; } // Print the list of instances (your module will probably extend this). $timenow = time(); $strname = get_string("name"); $usesections = course_format_uses_sections($course->format); $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index table table-hover'; if ($usesections) { $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname); $table->align = array ("center", "left"); } else { $table->head = array ($strname); } foreach ($basicltis as $basiclti) { if (!$basiclti->visible) { // Show dimmed if the mod is hidden. $link = "coursemodule\">$basiclti->name"; } else { // Show normal if the mod is visible. $link = "coursemodule\">$basiclti->name"; } if ($usesections) { $table->data[] = array (get_section_name($course, $basiclti->section), $link); } else { $table->data[] = array ($link); } } echo "
"; echo html_writer::table($table); // Finish the page. echo $OUTPUT->footer(); registration.php000064400000002633152265520610007777 0ustar00. /** * This file contains all necessary code to initiate a tool registration process * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $id = required_param('id', PARAM_INT); // Tool Proxy ID. $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id), '*', MUST_EXIST); require_login(0, false); require_sesskey(); $systemcontext = context_system::instance(); require_capability('moodle/site:config', $systemcontext); lti_register($toolproxy); registersettings.php000064400000006236152265520610010675 0ustar00. /** * This file contains the script used to register a new external tool. * * It is used to create a new form used to configure the capabilities * and services to be offered to the tool provider. * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/register_form.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $action = optional_param('action', null, PARAM_ALPHANUMEXT); $id = optional_param('id', null, PARAM_INT); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $returnto = optional_param('returnto', '', PARAM_ALPHA); if ($returnto == 'toolconfigure') { $returnurl = new moodle_url($CFG->wwwroot . '/mod/lti/toolconfigure.php'); } // No guest autologin. require_login(0, false); $isupdate = !empty($id); $pageurl = new moodle_url('/mod/lti/registersettings.php'); if ($isupdate) { $pageurl->param('id', $id); } if (!empty($returnto)) { $pageurl->param('returnto', $returnto); } $PAGE->set_url($pageurl); admin_externalpage_setup('ltitoolproxies'); $redirect = new moodle_url('/mod/lti/toolproxies.php', array('tab' => $tab)); $redirect = $redirect->out(); if (!empty($returnurl)) { $redirect = $returnurl; } require_sesskey(); if ($action == 'delete') { lti_delete_tool_proxy($id); redirect($redirect); } $data = array(); if ($isupdate) { $data['isupdate'] = true; } $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->set_secondary_active_tab('modules'); $form = new mod_lti_register_types_form($pageurl, (object)$data); if ($form->is_cancelled()) { redirect($redirect); } else if ($data = $form->get_data()) { $id = lti_add_tool_proxy($data); redirect($redirect); } else { $PAGE->set_title(get_string('toolregistration', 'lti')); $PAGE->navbar->add(get_string('lti_administration', 'lti'), $redirect); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('toolregistration', 'lti')); echo $OUTPUT->box_start('generalbox'); if ($action == 'update') { $toolproxy = lti_get_tool_proxy_config($id); $form->set_data($toolproxy); if ($toolproxy->state == LTI_TOOL_PROXY_STATE_ACCEPTED) { $form->disable_fields(); } } $form->display(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); } token.php000064400000006652152265520610006412 0ustar00. /** * This file contains a service for issuing access tokens * * @package mod_lti * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); use Firebase\JWT\JWT; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $response = new \mod_lti\local\ltiservice\response(); $contenttype = isset($_SERVER['CONTENT_TYPE']) ? explode(';', $_SERVER['CONTENT_TYPE'], 2)[0] : ''; $ok = ($_SERVER['REQUEST_METHOD'] === 'POST') && ($contenttype === 'application/x-www-form-urlencoded'); $error = 'invalid_request'; $clientassertion = optional_param('client_assertion', '', PARAM_TEXT); $clientassertiontype = optional_param('client_assertion_type', '', PARAM_TEXT); $granttype = optional_param('grant_type', '', PARAM_TEXT); $scope = optional_param('scope', '', PARAM_TEXT); if ($ok) { $ok = !empty($clientassertion) && !empty($clientassertiontype) && !empty($granttype) && !empty($scope); } if ($ok) { $ok = ($clientassertiontype === 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') && ($granttype === 'client_credentials'); $error = 'unsupported_grant_type'; } if ($ok) { $parts = explode('.', $clientassertion); $ok = (count($parts) === 3); if ($ok) { $payload = JWT::urlsafeB64Decode($parts[1]); $claims = json_decode($payload, true); $ok = !is_null($claims) && !empty($claims['sub']); } $error = 'invalid_request'; } if ($ok) { $tool = $DB->get_record('lti_types', array('clientid' => $claims['sub'])); if ($tool) { try { lti_verify_jwt_signature($tool->id, $claims['sub'], $clientassertion); $ok = true; } catch (Exception $e) { $error = $e->getMessage(); $ok = false; } } else { $error = 'invalid_client'; $ok = false; } } if ($ok) { $scopes = array(); $requestedscopes = explode(' ', $scope); $typeconfig = lti_get_type_config($tool->id); $permittedscopes = lti_get_permitted_service_scopes($tool, $typeconfig); $scopes = array_intersect($requestedscopes, $permittedscopes); $ok = !empty($scopes); $error = 'invalid_scope'; } if ($ok) { $token = lti_new_access_token($tool->id, $scopes); $expiry = LTI_ACCESS_TOKEN_LIFE; $permittedscopes = implode(' ', $scopes); $body = <<< EOD { "access_token" : "{$token->token}", "token_type" : "Bearer", "expires_in" : {$expiry}, "scope" : "{$permittedscopes}" } EOD; } else { $response->set_code(400); $body = <<< EOD { "error" : "{$error}" } EOD; } $response->set_body($body); $response->send();amd/src/tool_proxy.js000064400000005604152265520610010661 0ustar00// 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 . /** * Provides an interface for a tool proxy in the Moodle server. * * @module mod_lti/tool_proxy * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['core/ajax', 'core/notification'], function(ajax, notification) { return { /** * Get a list of tool types from Moodle for the given * search args. * * See also: * mod/lti/classes/external.php get_tool_types_parameters() * * @method query * @public * @param {Object} args Search parameters * @return {Promise} jQuery Deferred object */ query: function(args) { var request = { methodname: 'mod_lti_get_tool_proxies', args: args || {} }; var promise = ajax.call([request])[0]; promise.fail(notification.exception); return promise; }, /** * Delete a tool proxy from Moodle. * * @method delete * @public * @param {Integer} id Tool proxy ID * @return {Promise} jQuery Deferred object */ 'delete': function(id) { var request = { methodname: 'mod_lti_delete_tool_proxy', args: { id: id } }; var promise = ajax.call([request])[0]; promise.fail(notification.exception); return promise; }, /** * Create a tool proxy in Moodle. * * The promise will fail if the proxy cannot be created, so you must handle the fail result. * * See mod/lti/classes/external.php create_tool_proxy_parameters * * @method create * @public * @param {Object} args Tool proxy properties * @return {Promise} jQuery Deferred object */ create: function(args) { var request = { methodname: 'mod_lti_create_tool_proxy', args: args }; var promise = ajax.call([request])[0]; return promise; } }; }); amd/src/contentitem_return.js000064400000003111152265520610012362 0ustar00// 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 . /** * Processes the result of LTI tool creation from a Content-Item message type. * * @module mod_lti/contentitem_return * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define(['jquery'], function($) { return { /** * Init function. * * @param {string} returnData The returned data. */ init: function(returnData) { // Make sure the window has loaded before we perform processing. $(window).ready(function() { if (window != top) { // Send return data to be processed by the parent window. parent.processContentItemReturnData(returnData); } else { window.processContentItemReturnData(returnData); } }); } }; }); amd/src/cartridge_registration_form.js000064400000015000152265520610014213 0ustar00// 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 . /** * Encapsules the behavior for creating a tool type from a cartridge URL * in Moodle. Manages the UI while operations are occuring. * * See template: mod_lti/cartridge_registration_form * * @module mod_lti/cartridge_registration_form * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/notification', 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys', 'core/str'], function($, ajax, notification, toolType, ltiEvents, KEYS, str) { var SELECTORS = { CARTRIDGE_URL: '#cartridge-url', CONSUMER_KEY: '#registration-key', SHARED_SECRET: '#registration-secret', REGISTRATION_FORM: '#cartridge-registration-form', REGISTRATION_SUBMIT_BUTTON: '#cartridge-registration-submit', REGISTRATION_CANCEL_BUTTON: '#cartridge-registration-cancel', }; /** * Return the URL the user entered for the cartridge. * * @method getCartridgeURL * @private * @return {String} */ var getCartridgeURL = function() { return $(SELECTORS.REGISTRATION_FORM).attr('data-cartridge-url'); }; /** * Return the submit button element. * * @method getSubmitButton * @private * @return {JQuery} jQuery object */ var getSubmitButton = function() { return $(SELECTORS.REGISTRATION_SUBMIT_BUTTON); }; /** * Return the cancel button element. * * @method getCancelButton * @private * @return {JQuery} jQuery object */ var getCancelButton = function() { return $(SELECTORS.REGISTRATION_CANCEL_BUTTON); }; /** * Return the value that the user entered for the consumer key. * * @method getConsumerKey * @private * @return {String} the value entered for consumer key. */ var getConsumerKey = function() { return $(SELECTORS.CONSUMER_KEY).val(); }; /** * Return the value that the user entered for the shared secret. * * @method getSharedSecret * @private * @return {String} the value entered for shared secret */ var getSharedSecret = function() { return $(SELECTORS.SHARED_SECRET).val(); }; /** * Trigger a visual loading indicator. * * @method startLoading * @private */ var startLoading = function() { getSubmitButton().addClass('loading'); }; /** * Stop the visual loading indicator. * * @method stopLoading * @private */ var stopLoading = function() { getSubmitButton().removeClass('loading'); }; /** * Check if the page is currently loading. * * @method isLoading * @private * @return {Boolean} */ var isLoading = function() { return getSubmitButton().hasClass('loading'); }; /** * Create a tool type from the cartridge URL that the user input. This will * send an ajax request to the Moodle server to create the Type. The request will * include the consumer key and secret, if any. * * On success the page will be re-rendered to take the user back to the original * page with the list of tools and an alert notifying them of success. * * @method submitCartridgeURL * @private * @return {Promise} jQuery Deferred object */ var submitCartridgeURL = function() { if (isLoading()) { return false; } var url = getCartridgeURL(); // No URL? Do nothing. if (url === "") { return false; } startLoading(); var consumerKey = getConsumerKey(); var sharedSecret = getSharedSecret(); var promise = toolType.create({cartridgeurl: url, key: consumerKey, secret: sharedSecret}); promise.done(function() { str.get_string('successfullycreatedtooltype', 'mod_lti').done(function(s) { $(document).trigger(ltiEvents.NEW_TOOL_TYPE); $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION); $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, { message: s }); }).fail(notification.exception); }).fail(function() { str.get_string('failedtocreatetooltype', 'mod_lti').done(function(s) { $(document).trigger(ltiEvents.NEW_TOOL_TYPE); $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION); $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, { message: s, error: true }); }).fail(notification.exception); }).always(function() { stopLoading(); }); return promise; }; /** * Sets up the listeners for user interaction on the page. * * @method registerEventListeners * @private */ var registerEventListeners = function() { var form = $(SELECTORS.REGISTRATION_FORM); form.submit(function(e) { e.preventDefault(); submitCartridgeURL(); }); var cancelButton = getCancelButton(); cancelButton.click(function(e) { e.preventDefault(); $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION); }); cancelButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); cancelButton.click(); } } }); }; return /** @alias module:mod_lti/cartridge_registration_form */ { /** * Initialise this module. */ init: function() { registerEventListeners(); } }; }); amd/src/form-field.js000064400000010240152265520610010457 0ustar00// 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 . /** * A module that enables the setting of form field values on the client side. * * @module mod_lti/form-field * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define(['jquery'], function($) { /** * Form field class. * * @param {string} name Field name. * @param {number} type The field type. * @param {boolean} resetIfUndefined Flag to reset the field to the default value if undefined in the return data. * @param {string|number|boolean} defaultValue The default value to use for the field. * @constructor */ var FormField = function(name, type, resetIfUndefined, defaultValue) { this.name = name; this.id = 'id_' + this.name; this.selector = '#' + this.id; this.type = type; this.resetIfUndefined = resetIfUndefined; this.defaultValue = defaultValue; }; /** * Form field types. * * @type {{TEXT: number, SELECT: number, CHECKBOX: number, EDITOR: number}} */ FormField.TYPES = { TEXT: 1, SELECT: 2, CHECKBOX: 3, EDITOR: 4 }; /** * Sets the values for a form field. * * @param {string|boolean|number} value The value to be set into the field. */ FormField.prototype.setFieldValue = function(value) { if (value === null) { if (this.resetIfUndefined) { value = this.defaultValue; } else { // No need set the field value if value is null and there's no need to reset the field. return; } } switch (this.type) { case FormField.TYPES.CHECKBOX: if (value) { $(this.selector).prop('checked', true); } else { $(this.selector).prop('checked', false); } break; case FormField.TYPES.EDITOR: if (typeof value.text !== 'undefined') { /* global tinyMCE:false */ // Set text in editor's editable content, if applicable. // Check if it is an Atto editor. var attoEditor = $(this.selector + 'editable'); if (attoEditor.length) { attoEditor.html(value.text); } else if (typeof tinyMCE !== 'undefined') { // If the editor is not Atto, try to fallback to TinyMCE. if (tinyMCE.majorVersion == "3") { // Tiny 3. tinyMCE.execInstanceCommand(this.id, 'mceInsertContent', false, value.text); } else { // Tiny 4+. tinyMCE.get(this.id).setContent(value.text); } } // Set text to actual editor text area. $(this.selector).val(value.text); } break; default: $(this.selector).val(value); break; } }; return FormField; } ); amd/src/contentitem.js000064400000025611152265520610010774 0ustar00// 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 . /** * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an * LTI tool provider that supports Content-Item type message. * * See template: mod_lti/contentitem * * @module mod_lti/contentitem * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define( [ 'jquery', 'core/notification', 'core/str', 'core/templates', 'mod_lti/form-field', 'core/modal', 'core/modal_events' ], function($, notification, str, templates, FormField, Modal, ModalEvents) { var dialogue; var doneCallback; var contentItem = { /** * Init function. * * @param {string} url The URL for the content item selection. * @param {string} toolUrl The URL of the LTI tool. * @param {object} postData The data to be sent for the content item selection request. * @param {Function} cb The callback to run once the content item has been processed. */ init: function(url, toolUrl, postData, cb) { doneCallback = cb; var context = { url: url, toolUrl: toolUrl, postData: postData }; var bodyPromise = templates.render('mod_lti/contentitem', context); if (dialogue) { // Set dialogue body. dialogue.setBody(bodyPromise); // Display the dialogue. dialogue.show(); return; } str.get_string('selectcontent', 'lti').then(function(title) { return Modal.create({ title: title, body: bodyPromise, large: true, show: true, }); }).then(function(modal) { dialogue = modal; // On hide handler. modal.getRoot().on(ModalEvents.hidden, function() { // Empty modal contents when it's hidden. modal.setBody(''); // Fetch notifications. notification.fetchNotifications(); }); return; }).catch(notification.exception); } }; /** * Array of form fields for LTI tool configuration. */ var ltiFormFields = [ new FormField('name', FormField.TYPES.TEXT, false, ''), new FormField('introeditor', FormField.TYPES.EDITOR, false, ''), new FormField('toolurl', FormField.TYPES.TEXT, true, ''), new FormField('securetoolurl', FormField.TYPES.TEXT, true, ''), new FormField('instructorchoiceacceptgrades', FormField.TYPES.CHECKBOX, true, true), new FormField('instructorchoicesendname', FormField.TYPES.CHECKBOX, true, true), new FormField('instructorchoicesendemailaddr', FormField.TYPES.CHECKBOX, true, true), new FormField('instructorcustomparameters', FormField.TYPES.TEXT, true, ''), new FormField('icon', FormField.TYPES.TEXT, true, ''), new FormField('secureicon', FormField.TYPES.TEXT, true, ''), new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0), new FormField('grade_modgrade_point', FormField.TYPES.TEXT, false, ''), new FormField('lineitemresourceid', FormField.TYPES.TEXT, true, ''), new FormField('lineitemtag', FormField.TYPES.TEXT, true, ''), new FormField('lineitemsubreviewurl', FormField.TYPES.TEXT, true, ''), new FormField('lineitemsubreviewparams', FormField.TYPES.TEXT, true, '') ]; /** * Hide the element, including aria and tab index. * @param {HTMLElement} e the element to be hidden. */ const hideElement = (e) => { e.setAttribute('hidden', 'true'); e.setAttribute('aria-hidden', 'true'); e.setAttribute('tab-index', '-1'); }; /** * Show the element, including aria and tab index (set to 1). * @param {HTMLElement} e the element to be shown. */ const showElement = (e) => { e.removeAttribute('hidden'); e.setAttribute('aria-hidden', 'false'); e.setAttribute('tab-index', '1'); }; /** * When more than one item needs to be added, the UI is simplified * to just list the items to be added. Form is hidden and the only * options is (save and return to course) or cancel. * This function injects the summary to the form page, and hides * the unneeded elements. * @param {Object[]} items items to be added to the course. */ const showMultipleSummaryAndHideForm = async function(items) { const form = document.querySelector('#region-main-box form'); const toolArea = form.querySelector('[data-attribute="dynamic-import"]'); const buttonGroup = form.querySelector('#fgroup_id_buttonar'); const submitAndLaunch = form.querySelector('#id_submitbutton'); Array.from(form.children).forEach(hideElement); hideElement(submitAndLaunch); const {html, js} = await templates.renderForPromise('mod_lti/tool_deeplinking_results', {items: items}); await templates.replaceNodeContents(toolArea, html, js); showElement(toolArea); showElement(buttonGroup); }; /** * Transforms config values aimed at populating the lti mod form to JSON variant * which are used to insert more than one activity modules in one submit * by applying variation to the submitted form. * See /course/modedit.php. * @private * @param {Object} config transforms a config to an actual form data to be posted. * @return {Object} variant that will be used to modify form values on submit. */ var configToVariant = (config) => { const variant = {}; ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon', 'launchcontainer', 'lineitemresourceid', 'lineitemtag', 'lineitemsubreviewurl', 'lineitemsubreviewparams'].forEach( function(name) { variant[name] = config[name] || ''; } ); variant['introeditor[text]'] = config.introeditor ? config.introeditor.text : ''; variant['introeditor[format]'] = config.introeditor ? config.introeditor.format : ''; if (config.instructorchoiceacceptgrades === 1) { variant.instructorchoiceacceptgrades = '1'; variant['grade[modgrade_point]'] = config.grade_modgrade_point || '100'; } else { variant.instructorchoiceacceptgrades = '0'; } return variant; }; /** * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data. * If the return data contains more than one item, the form will not be populated with item data * but rather hidden, and the item data will be added to a single input field used to create multiple * instances in one request. * * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue. */ window.processContentItemReturnData = function(returnData) { if (dialogue) { dialogue.hide(); } var index; if (returnData.multiple) { for (index in ltiFormFields) { // Name is required, so putting a placeholder as it will not be used // in multi-items add. ltiFormFields[index].setFieldValue(ltiFormFields[index].name === 'name' ? 'item' : null); } var variants = []; returnData.multiple.forEach(function(v) { variants.push(configToVariant(v)); }); showMultipleSummaryAndHideForm(returnData.multiple); const submitAndCourse = document.querySelector('#id_submitbutton2'); submitAndCourse.onclick = (e) => { e.preventDefault(); submitAndCourse.disabled = true; const fd = new FormData(document.querySelector('#region-main-box form')); const postVariant = (promise, variant) => { Object.entries(variant).forEach((entry) => fd.set(entry[0], entry[1])); const body = new URLSearchParams(fd); const doPost = () => fetch(document.location.pathname, {method: 'post', body}); return promise.then(doPost).catch(doPost); }; const backToCourse = () => { document.querySelector("#id_cancel").click(); }; variants.reduce(postVariant, Promise.resolve()).then(backToCourse).catch(backToCourse); }; } else { // Populate LTI configuration fields from return data. for (index in ltiFormFields) { var field = ltiFormFields[index]; var value = null; if (typeof returnData[field.name] !== 'undefined') { value = returnData[field.name]; } field.setFieldValue(value); } field.setFieldValue(value); // Update the UI element which signifies content has been selected. document.querySelector("#id_selectcontentindicator").innerHTML = returnData.selectcontentindicator; } if (doneCallback) { doneCallback(returnData); } }; return contentItem; } ); amd/src/coursecategory.js000064400000010755152265520610011504 0ustar00// 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 . /** * Select course categories for LTI tool. * * @module mod_lti/coursecategory * @copyright 2023 Jackson D'souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.3 */ define([], function() { document.addEventListener('click', event => { const checkedbox = event.target.closest(".lticoursecategories"); if (checkedbox) { // Get checkbox status. const checkboxstatus = checkedbox.checked; // Check / Uncheck all child category checkboxes based on selected checkbox status. const categorycontainer = document.querySelector('#collapse' + checkedbox.value); if (categorycontainer) { const categorycontainercheckbox = categorycontainer.querySelectorAll('input[type="checkbox"]'); for (let i = 0; i < categorycontainercheckbox.length; i++) { categorycontainercheckbox[i].checked = checkboxstatus; } } const lticategorytree = document.querySelector('.modltitree'); const ltitreecheckbox = lticategorytree.querySelectorAll('input[type="checkbox"]'); let listvalue = ''; for (let i = 0; i < ltitreecheckbox.length; i++) { if (ltitreecheckbox[i].checked) { if (listvalue.length == 0) { listvalue = ltitreecheckbox[i].value; } else { listvalue = listvalue + ',' + ltitreecheckbox[i].value; } } } document.querySelector('input[name="lti_coursecategories"]').value = listvalue; } }); /** * Get parent elements with class = accordion. * * @method getParents * @private * @param {string} elem DOM element. * @return {array} Parent elements. */ function getParents(elem) { // Set up a parent array const parents = []; // Push each parent element to the array for (; elem && elem !== document; elem = elem.parentNode) { if (elem.classList.contains('accordion-group')) { parents.push(elem); } } // Return our parent array return parents; } return /** @alias module:mod_lti/coursecategory */ { /** * Initialise this module. * Loop through checkbox form elements starting with #cat-{N} and set it to checked * if {N} is found in the Selected category(s) list. Show / Hide the parent UL element. * * @param {string} selectedcategories Selected category(s). */ init: function(selectedcategories) { if (selectedcategories.length) { const separator = ","; const values = selectedcategories.split(separator); for (let i = 0; i < values.length; i++) { const categoryid = document.getElementById("cat-" + values[i]); if (categoryid.value !== 0) { categoryid.checked = true; } const parents = getParents(categoryid); parents.forEach(function(element) { const elem = element.querySelector('a.accordion-toggle'); const elembody = element.querySelector('.accordion-body'); if (elem && elem.classList.contains('collapsed')) { elem.classList.remove('collapsed'); } if (elembody) { elembody.classList.remove('collapse'); elembody.classList.add('show'); } }); } } } }; });amd/src/events.js000064400000003052152265520610007742 0ustar00// 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 . /** * Provides a list of events that can be triggered in the LTI management * page. * * @module mod_lti/events * @class events * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define([], function() { return /** @alias module:mod_lti/events */ { NEW_TOOL_TYPE: 'lti.tool.type.new', START_EXTERNAL_REGISTRATION: 'lti.registration.external.start', STOP_EXTERNAL_REGISTRATION: 'lti.registration.external.stop', START_CARTRIDGE_REGISTRATION: 'lti.registration.cartridge.start', STOP_CARTRIDGE_REGISTRATION: 'lti.registration.cartridge.stop', REGISTRATION_FEEDBACK: 'lti.registration.feedback', CAPABILITIES_AGREE: 'lti.tool.type.capabilities.agree', CAPABILITIES_DECLINE: 'lti.tool.type.capabilities.decline', }; }); amd/src/course_tools_list.js000064400000010171152265520610012211 0ustar00// 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 . /** * Course LTI External tools list management. * * @module mod_lti/course_tools_list * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; import Notification from 'core/notification'; import Pending from 'core/pending'; import Ajax from 'core/ajax'; import {add as addToast} from 'core/toast'; import {getString, getStrings} from 'core/str'; import {refreshTableContent} from 'core_table/dynamic'; import * as Selectors from 'core_table/local/dynamic/selectors'; import {toggleShowInActivityChooser} from "./repository"; /** * Initialise module. */ export const init = () => { document.addEventListener('click', event => { const courseToolDelete = event.target.closest('[data-action="course-tool-delete"]'); if (courseToolDelete) { event.preventDefault(); // A different message is used in the modal if the tool has usages within the course. const usage = courseToolDelete.dataset.courseToolUsage; const deleteBodyStringId = usage > 0 ? 'deletecoursetoolwithusageconfirm' : 'deletecoursetoolconfirm'; const requiredStrings = [ {key: 'deletecoursetool', component: 'mod_lti', param: courseToolDelete.dataset.courseToolName}, {key: deleteBodyStringId, component: 'mod_lti', param: courseToolDelete.dataset.courseToolName}, {key: 'delete', component: 'core', param: courseToolDelete.dataset.courseToolName}, {key: 'coursetooldeleted', component: 'mod_lti', param: courseToolDelete.dataset.courseToolName} ]; // Use triggerElement to return focus to the action menu toggle. const triggerElement = courseToolDelete.closest('.dropdown').querySelector('.dropdown-toggle'); getStrings(requiredStrings).then(([modalTitle, modalBody, deleteLabel]) => { return Notification.deleteCancelPromise( modalTitle, modalBody, deleteLabel, {triggerElement}); }).then(() => { const pendingPromise = new Pending('mod_lti/course_tools:delete'); const request = { methodname: 'mod_lti_delete_course_tool_type', args: {tooltypeid: courseToolDelete.dataset.courseToolId} }; return Ajax.call([request])[0] .then(addToast(getString('coursetooldeleted', 'mod_lti', courseToolDelete.dataset.courseToolName))) .then(() => { const tableRoot = triggerElement.closest(Selectors.main.region); return refreshTableContent(tableRoot); }) .then(pendingPromise.resolve) .catch(Notification.exception); }).catch(() => { return; }); } const courseShowInActivityChooser = event.target.closest('[data-action="showinactivitychooser-toggle"]'); if (courseShowInActivityChooser) { const showInActivityChooserStateToggle = courseShowInActivityChooser.dataset.state === "0" ? 1 : 0; return toggleShowInActivityChooser( courseShowInActivityChooser.dataset.id, courseShowInActivityChooser.dataset.courseid, showInActivityChooserStateToggle, ); } }); }; amd/src/keys.js000064400000002062152265520610007411 0ustar00// 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 . /** * A list of keys and their keycodes that are used by the LTI modules. * * @module mod_lti/keys * @class keys * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define([], function() { return /** @alias module:mod_lti/keys */ { ENTER: 13, SPACE: 32 }; }); amd/src/tool_card_controller.js000064400000055714152265520610012663 0ustar00// 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 . /** * Controls all of the behaviour and interaction with a tool type card. These are * listed on the LTI tool type management page. * * See template: mod_lti/tool_card * * @module mod_lti/tool_card_controller * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'core/modal', 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys', 'core/str'], function($, ajax, notification, templates, Modal, toolType, ltiEvents, KEYS, str) { var SELECTORS = { DELETE_BUTTON: '.delete', NAME_ELEMENT: '.name', DESCRIPTION_ELEMENT: '.description', CAPABILITIES_CONTAINER: '.capabilities-container', ACTIVATE_BUTTON: '.tool-card-footer a.activate', }; // Timeout in seconds. var ANNOUNCEMENT_TIMEOUT = 2000; /** * Return the delete button element. * * @method getDeleteButton * @private * @param {JQuery} element jQuery object representing the tool card. * @return {JQuery} jQuery object */ var getDeleteButton = function(element) { return element.find(SELECTORS.DELETE_BUTTON); }; /** * Return the element representing the tool type name. * * @method getNameElement * @private * @param {JQuery} element jQuery object representing the tool card. * @return {JQuery} jQuery object */ var getNameElement = function(element) { return element.find(SELECTORS.NAME_ELEMENT); }; /** * Return the element representing the tool type description. * * @method getDescriptionElement * @private * @param {JQuery} element jQuery object representing the tool card. * @return {JQuery} jQuery object */ var getDescriptionElement = function(element) { return element.find(SELECTORS.DESCRIPTION_ELEMENT); }; /** * Return the activate button for the type. * * @method getActivateButton * @private * @param {Object} element jQuery object representing the tool card. * @return {Object} jQuery object */ var getActivateButton = function(element) { return element.find(SELECTORS.ACTIVATE_BUTTON); }; /** * Checks if the type card has an activate button. * * @method hasActivateButton * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Boolean} true if has active buton */ var hasActivateButton = function(element) { return getActivateButton(element).length ? true : false; }; /** * Return the element that contains the capabilities approval for * the user. * * @method getCapabilitiesContainer * @private * @param {Object} element jQuery object representing the tool card. * @return {Object} The element */ var getCapabilitiesContainer = function(element) { return element.find(SELECTORS.CAPABILITIES_CONTAINER); }; /** * Checks if the tool type has capabilities that need approval. If it * does then the container will be present. * * @method hasCapabilitiesContainer * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Boolean} true if has capbilities. */ var hasCapabilitiesContainer = function(element) { return getCapabilitiesContainer(element).length ? true : false; }; /** * Get the type id. * * @method getTypeId * @private * @param {Object} element jQuery object representing the tool card. * @return {String} Type ID */ var getTypeId = function(element) { return element.attr('data-type-id'); }; /** * Stop any announcement currently visible on the card. * * @method clearAllAnnouncements * @private * @param {JQuery} element jQuery object representing the tool card. */ var clearAllAnnouncements = function(element) { element.removeClass('announcement loading success fail capabilities'); }; /** * Show the loading announcement. * * @method startLoading * @private * @param {JQuery} element jQuery object representing the tool card. */ var startLoading = function(element) { clearAllAnnouncements(element); element.addClass('announcement loading'); }; /** * Hide the loading announcement. * * @method stopLoading * @private * @param {JQuery} element jQuery object representing the tool card. */ var stopLoading = function(element) { element.removeClass('announcement loading'); }; /** * Show the success announcement. The announcement is only * visible for 2 seconds. * * @method announceSuccess * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var announceSuccess = function(element) { var promise = $.Deferred(); clearAllAnnouncements(element); element.addClass('announcement success'); setTimeout(function() { element.removeClass('announcement success'); promise.resolve(); }, ANNOUNCEMENT_TIMEOUT); return promise; }; /** * Show the failure announcement. The announcement is only * visible for 2 seconds. * * @method announceFailure * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var announceFailure = function(element) { var promise = $.Deferred(); clearAllAnnouncements(element); element.addClass('announcement fail'); setTimeout(function() { element.removeClass('announcement fail'); promise.resolve(); }, ANNOUNCEMENT_TIMEOUT); return promise; }; /** * Delete the tool type from the Moodle server. Triggers a success * or failure announcement depending on the result. * * @method deleteType * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var deleteType = function(element) { var promise = $.Deferred(); var typeId = getTypeId(element); startLoading(element); if (typeId === "") { return $.Deferred().resolve(); } str.get_strings([ { key: 'delete', component: 'mod_lti' }, { key: 'delete_confirmation', component: 'mod_lti' }, { key: 'delete', component: 'mod_lti' }, { key: 'cancel', component: 'core' }, ]) .done(function(strs) { notification.confirm(strs[0], strs[1], strs[2], strs[3], function() { toolType.delete(typeId) .done(function() { stopLoading(element); announceSuccess(element) .done(function() { element.remove(); }) .fail(notification.exception) .always(function() { // Always resolve because even if the announcement fails the type was deleted. promise.resolve(); }); }) .fail(function(error) { announceFailure(element); promise.reject(error); }); }, function() { stopLoading(element); promise.resolve(); }); }) .fail(function(error) { stopLoading(element); notification.exception(error); promise.reject(error); }); return promise; }; /** * Save a given value in a data attribute on the element. * * @method setValueSnapshot * @private * @param {JQuery} element jQuery object representing the element. * @param {String} value to be saved. */ var setValueSnapshot = function(element, value) { element.attr('data-val-snapshot', value); }; /** * Return the saved value from the element. * * @method getValueSnapshot * @private * @param {JQuery} element jQuery object representing the element. * @return {String} the saved value. */ var getValueSnapshot = function(element) { return element.attr('data-val-snapshot'); }; /** * Save the current value of the tool description. * * @method snapshotDescription * @private * @param {JQuery} element jQuery object representing the tool card. */ var snapshotDescription = function(element) { var descriptionElement = getDescriptionElement(element); if (descriptionElement.hasClass('loading')) { return; } var description = descriptionElement.text().trim(); setValueSnapshot(descriptionElement, description); }; /** * Send a request to update the description value for this tool * in the Moodle server. * * @method updateDescription * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var updateDescription = function(element) { var typeId = getTypeId(element); // Return early if we don't have an id because it's // required to save the changes. if (typeId === "") { return $.Deferred().resolve(); } var descriptionElement = getDescriptionElement(element); // Return early if we're already saving a value. if (descriptionElement.hasClass('loading')) { return $.Deferred().resolve(); } var description = descriptionElement.text().trim(); var snapshotVal = getValueSnapshot(descriptionElement); // If the value hasn't change then don't bother sending the // update request. if (snapshotVal == description) { return $.Deferred().resolve(); } descriptionElement.addClass('loading'); var promise = toolType.update({id: typeId, description: description}); promise.done(function(type) { descriptionElement.removeClass('loading'); // Make sure the text is updated with the description from the // server, just in case the update didn't work. descriptionElement.text(type.description); }).fail(notification.exception); // Probably need to handle failures better so that we can revert // the value in the input for the user. promise.fail(function() { descriptionElement.removeClass('loading'); }); return promise; }; /** * Save the current value of the tool name. * * @method snapshotName * @private * @param {JQuery} element jQuery object representing the tool card. */ var snapshotName = function(element) { var nameElement = getNameElement(element); if (nameElement.hasClass('loading')) { return; } var name = nameElement.text().trim(); setValueSnapshot(nameElement, name); }; /** * Send a request to update the name value for this tool * in the Moodle server. * * @method updateName * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var updateName = function(element) { var typeId = getTypeId(element); // Return if we don't have an id. if (typeId === "") { return $.Deferred().resolve(); } var nameElement = getNameElement(element); // Return if we're already saving. if (nameElement.hasClass('loading')) { return $.Deferred().resolve(); } var name = nameElement.text().trim(); var snapshotVal = getValueSnapshot(nameElement); // If the value hasn't change then don't bother sending the // update request. if (snapshotVal == name) { return $.Deferred().resolve(); } nameElement.addClass('loading'); var promise = toolType.update({id: typeId, name: name}); promise.done(function(type) { nameElement.removeClass('loading'); // Make sure the text is updated with the name from the // server, just in case the update didn't work. nameElement.text(type.name); }); // Probably need to handle failures better so that we can revert // the value in the input for the user. promise.fail(function() { nameElement.removeClass('loading'); }); return promise; }; /** * Send a request to update the state for this tool to be configured (active) * in the Moodle server. A success or failure announcement is triggered depending * on the result. * * @method setStatusActive * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var setStatusActive = function(element) { var id = getTypeId(element); // Return if we don't have an id. if (id === "") { return $.Deferred().resolve(); } startLoading(element); var promise = toolType.update({ id: id, state: toolType.constants.state.configured }); promise.then(function(toolTypeData) { stopLoading(element); announceSuccess(element); return toolTypeData; }).then(function(toolTypeData) { return templates.render('mod_lti/tool_card', toolTypeData); }).then(function(html, js) { templates.replaceNode(element, html, js); return; }).catch(function() { stopLoading(element); announceFailure(element); }); return promise; }; /** * Show the capabilities approval screen to show which groups of data this * type requires access to in Moodle (if any). * * @method displayCapabilitiesApproval * @private * @param {JQuery} element jQuery object representing the tool card. */ var displayCapabilitiesApproval = function(element) { element.addClass('announcement capabilities'); }; /** * Hide the capabilities approval screen. * * @method hideCapabilitiesApproval * @private * @param {JQuery} element jQuery object representing the tool card. */ var hideCapabilitiesApproval = function(element) { element.removeClass('announcement capabilities'); }; /** * The user wishes to activate this tool so show them the capabilities that * they need to agree to or if there are none then set the tool type's state * to active. * * @method activateToolType * @private * @param {JQuery} element jQuery object representing the tool card. */ var activateToolType = function(element) { if (hasCapabilitiesContainer(element)) { displayCapabilitiesApproval(element); } else { setStatusActive(element); } }; /** * Sets up the listeners for user interaction on this tool type card. * * @method registerEventListeners * @private * @param {JQuery} element jQuery object representing the tool card. */ var registerEventListeners = function(element) { var deleteButton = getDeleteButton(element); deleteButton.click(function(e) { e.preventDefault(); deleteType(element); }); deleteButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); deleteButton.click(); } } }); var descriptionElement = getDescriptionElement(element); descriptionElement.focus(function(e) { e.preventDefault(); // Save a copy of the current value for the description so that // we can check if the user has changed it before sending a request to // the server. snapshotDescription(element); }); descriptionElement.blur(function(e) { e.preventDefault(); updateDescription(element); }); descriptionElement.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER) { e.preventDefault(); descriptionElement.blur(); } } }); var nameElement = getNameElement(element); nameElement.focus(function(e) { e.preventDefault(); // Save a copy of the current value for the name so that // we can check if the user has changed it before sending a request to // the server. snapshotName(element); }); nameElement.blur(function(e) { e.preventDefault(); updateName(element); }); nameElement.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER) { e.preventDefault(); nameElement.blur(); } } }); // Only pending tool type cards have an activate button. if (hasActivateButton(element)) { var activateButton = getActivateButton(element); activateButton.click(function(e) { e.preventDefault(); activateToolType(element); }); activateButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); activateButton.click(); } } }); } if (hasCapabilitiesContainer(element)) { var capabilitiesContainer = getCapabilitiesContainer(element); capabilitiesContainer.on(ltiEvents.CAPABILITIES_AGREE, function() { setStatusActive(element); }); capabilitiesContainer.on(ltiEvents.CAPABILITIES_DECLINE, function() { hideCapabilitiesApproval(element); }); } }; /** * Sets up the templates for the tool configuration modal on this tool type card. * * @method registerModal * @private * @param {JQuery} element jQuery object representing the tool card. */ var registerModal = function(element) { const configurationLink = element.find('#' + element.data('uniqid') + '-' + element.data('deploymentid')); if (!configurationLink.length) { return; } const trigger = configurationLink.get(0); trigger.addEventListener('click', (e) => { e.preventDefault(); var context = { 'uniqid': element.data('uniqid'), 'platformid': element.data('platformid'), 'clientid': element.data('clientid'), 'deploymentid': element.data('deploymentid'), 'urls': { 'publickeyset': element.data('publickeyseturl'), 'accesstoken': element.data('accesstokenurl'), 'authrequest': element.data('authrequesturl') } }; var bodyPromise = templates.render('mod_lti/tool_config_modal_body', context); var mailTo = 'mailto:?subject=' + encodeURIComponent(element.data('mailtosubject')) + '&body=' + encodeURIComponent(element.data('platformidstr')) + ':%20' + encodeURIComponent(element.data('platformid')) + '%0D%0A' + encodeURIComponent(element.data('clientidstr')) + ':%20' + encodeURIComponent(element.data('clientid')) + '%0D%0A' + encodeURIComponent(element.data('deploymentidstr')) + ':%20' + encodeURIComponent(element.data('deploymentid')) + '%0D%0A' + encodeURIComponent(element.data('publickeyseturlstr')) + ':%20' + encodeURIComponent(element.data('publickeyseturl')) + '%0D%0A' + encodeURIComponent(element.data('accesstokenurlstr')) + ':%20' + encodeURIComponent(element.data('accesstokenurl')) + '%0D%0A' + encodeURIComponent(element.data('authrequesturlstr')) + ':%20' + encodeURIComponent(element.data('authrequesturl')) + '%0D%0A'; context = { 'mailto': mailTo }; var footerPromise = templates.render('mod_lti/tool_config_modal_footer', context); Modal.create({ large: true, title: element.data('modaltitle'), body: bodyPromise, footer: footerPromise, show: true }); }); }; return /** @alias module:mod_lti/tool_card_controller */ { /** * Initialise this module. * * @param {JQuery} element jQuery object representing the tool card. */ init: function(element) { registerEventListeners(element); registerModal(element); } }; }); amd/src/tool_configure_controller.js000064400000040343152265520610013723 0ustar00// 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 . /** * Standard Ajax wrapper for Moodle. It calls the central Ajax script, * which can call any existing webservice using the current session. * In addition, it can batch multiple requests and return multiple responses. * * @module mod_lti/tool_configure_controller * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/paged_content_factory', 'core/notification', 'core/templates', 'mod_lti/events', 'mod_lti/keys', 'mod_lti/tool_types_and_proxies', 'mod_lti/tool_type', 'mod_lti/tool_proxy', 'core/str', 'core/config'], function($, ajax, pagedContentFactory, notification, templates, ltiEvents, KEYS, toolTypesAndProxies, toolType, toolProxy, str, config) { var SELECTORS = { EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-container', EXTERNAL_REGISTRATION_PAGE_CONTAINER: '#external-registration-page-container', EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER: '#external-registration-template-container', CARTRIDGE_REGISTRATION_CONTAINER: '#cartridge-registration-container', CARTRIDGE_REGISTRATION_FORM: '#cartridge-registration-form', ADD_TOOL_FORM: '#add-tool-form', TOOL_CARD_CONTAINER: '#tool-card-container', TOOL_LIST_CONTAINER: '#tool-list-container', TOOL_CREATE_BUTTON: '#tool-create-button', TOOL_CREATE_LTILEGACY_BUTTON: '#tool-createltilegacy-button', REGISTRATION_CHOICE_CONTAINER: '#registration-choice-container', TOOL_URL: '#tool-url' }; /** * Get the tool list container element. * * @method getToolListContainer * @private * @return {Object} jQuery object */ var getToolListContainer = function() { return $(SELECTORS.TOOL_LIST_CONTAINER); }; /** * Get the tool card container element. * * @method getToolCardContainer * @private * @return {Object} jQuery object */ const getToolCardContainer = function() { return $(SELECTORS.TOOL_CARD_CONTAINER); }; /** * Get the external registration container element. * * @method getExternalRegistrationContainer * @private * @return {Object} jQuery object */ var getExternalRegistrationContainer = function() { return $(SELECTORS.EXTERNAL_REGISTRATION_CONTAINER); }; /** * Get the cartridge registration container element. * * @method getCartridgeRegistrationContainer * @private * @return {Object} jQuery object */ var getCartridgeRegistrationContainer = function() { return $(SELECTORS.CARTRIDGE_REGISTRATION_CONTAINER); }; /** * Get the registration choice container element. * * @method getRegistrationChoiceContainer * @private * @return {Object} jQuery object */ var getRegistrationChoiceContainer = function() { return $(SELECTORS.REGISTRATION_CHOICE_CONTAINER); }; /** * Close the LTI Advantage Registration IFrame. * * @private * @param {Object} e post message event sent from the registration frame. */ var closeLTIAdvRegistration = function(e) { if (e.data && 'org.imsglobal.lti.close' === e.data.subject) { $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty(); hideExternalRegistration(); showRegistrationChoices(); showToolList(); showRegistrationChoices(); reloadToolList(); } }; /** * Load the external registration template and render it in the DOM and display it. * * @method initiateRegistration * @private * @param {String} url where to send the registration request */ var initiateRegistration = function(url) { // Show the external registration page in an iframe. $(SELECTORS.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass('hidden'); var container = $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER); container.append($("")); showExternalRegistration(); window.addEventListener("message", closeLTIAdvRegistration, false); }; /** * Get the tool type URL. * * @method getToolURL * @private * @return {String} the tool type url */ var getToolURL = function() { return $(SELECTORS.TOOL_URL).val(); }; /** * Hide the external registration container. * * @method hideExternalRegistration * @private */ var hideExternalRegistration = function() { getExternalRegistrationContainer().addClass('hidden'); }; /** * Hide the cartridge registration container. * * @method hideCartridgeRegistration * @private */ var hideCartridgeRegistration = function() { getCartridgeRegistrationContainer().addClass('hidden'); }; /** * Hide the registration choice container. * * @method hideRegistrationChoices * @private */ var hideRegistrationChoices = function() { getRegistrationChoiceContainer().addClass('hidden'); }; /** * Display the external registration panel and hides the other * panels. * * @method showExternalRegistration * @private */ var showExternalRegistration = function() { hideCartridgeRegistration(); hideRegistrationChoices(); getExternalRegistrationContainer().removeClass('hidden'); screenReaderAnnounce(getExternalRegistrationContainer()); }; /** * Display the cartridge registration panel and hides the other * panels. * * @method showCartridgeRegistration * @param {String} url * @private */ var showCartridgeRegistration = function(url) { hideExternalRegistration(); hideRegistrationChoices(); // Don't save the key and secret from the last tool. var container = getCartridgeRegistrationContainer(); container.find('input').val(''); container.removeClass('hidden'); container.find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).attr('data-cartridge-url', url); screenReaderAnnounce(container); }; /** * Display the registration choices panel and hides the other * panels. * * @method showRegistrationChoices * @private */ var showRegistrationChoices = function() { hideExternalRegistration(); hideCartridgeRegistration(); getRegistrationChoiceContainer().removeClass('hidden'); screenReaderAnnounce(getRegistrationChoiceContainer()); }; /** * JAWS does not notice visibility changes with aria-live. * Remove and add the content back to force it to read it out. * This function can be removed once JAWS supports visibility. * * @method screenReaderAnnounce * @param {Object} element * @private */ var screenReaderAnnounce = function(element) { var children = element.children().detach(); children.appendTo(element); }; /** * Hides the list of tool types. * * @method hideToolList * @private */ var hideToolList = function() { getToolListContainer().addClass('hidden'); }; /** * Display the list of tool types. * * @method hideToolList * @private */ var showToolList = function() { getToolListContainer().removeClass('hidden'); }; /** * Display the registration feedback alert and hide the other panels. * * @method showRegistrationFeedback * @param {Object} data * @private */ var showRegistrationFeedback = function(data) { var type = data.error ? 'error' : 'success'; notification.addNotification({ message: data.message, type: type }); }; /** * Show the loading animation * * @method startLoading * @private * @param {Object} element jQuery object */ var startLoading = function(element) { element.addClass("loading"); }; /** * Hide the loading animation * * @method stopLoading * @private * @param {Object} element jQuery object */ var stopLoading = function(element) { element.removeClass("loading"); }; /** * Refresh the list of tool types and render the new ones. * * @method reloadToolList * @private */ var reloadToolList = function() { // Behat tests should wait for the tool list to load. M.util.js_pending('reloadToolList'); const cardContainer = getToolCardContainer(); const listContainer = getToolListContainer(); const limit = 60; // Get initial data with zero limit and offset. fetchToolCount().done(function(data) { pagedContentFactory.createWithTotalAndLimit( data.count, limit, function(pagesData) { return pagesData.map(function(pageData) { return fetchToolData(pageData.limit, pageData.offset) .then(function(data) { return renderToolData(data); }); }); }, { 'showFirstLast': true }) .done(function(html, js) { // Add the paged content into the page. templates.replaceNodeContents(cardContainer, html, js); }) .always(function() { stopLoading(listContainer); M.util.js_complete('reloadToolList'); }); }); startLoading(listContainer); }; /** * Fetch the count of tool type and proxy datasets. * * @return {*|void} */ const fetchToolCount = function() { return toolTypesAndProxies.count({'orphanedonly': true}) .done(function(data) { return data; }).catch(function(error) { // Add debug message, then return empty data. notification.exception(error); return { 'count': 0 }; }); }; /** * Fetch the data for tool type and proxy cards. * * @param {number} limit Maximum number of datasets to get. * @param {number} offset Offset count for fetching the data. * @return {*|void} */ const fetchToolData = function(limit, offset) { const args = {'orphanedonly': true}; // Only add limit and offset to args if they are integers and not null, otherwise defaults will be used. if (limit !== null && !Number.isNaN(limit)) { args.limit = limit; } if (offset !== null && !Number.isNaN(offset)) { args.offset = offset; } return toolTypesAndProxies.query(args) .done(function(data) { return data; }).catch(function(error) { // Add debug message, then return empty data. notification.exception(error); return { 'types': [], 'proxies': [], 'limit': limit, 'offset': offset }; }); }; /** * Render Tool and Proxy cards from data. * * @param {Object} data Contains arrays of data objects to populate cards. * @return {*} */ const renderToolData = function(data) { const context = { tools: data.types, proxies: data.proxies, }; return templates.render('mod_lti/tool_list', context) .done(function(html, js) { return {html, js}; } ); }; /** * Start the LTI Advantage registration. * * @method addLTIAdvTool * @private */ var addLTIAdvTool = function() { var url = getToolURL().trim(); if (url) { $(SELECTORS.TOOL_URL).val(''); hideToolList(); initiateRegistration(url); } }; /** * Trigger appropriate registration process process for the user input * URL. It can either be a cartridge or a registration url. * * @method addLTILegacyTool * @private * @return {Promise} jQuery Deferred object */ var addLTILegacyTool = function() { var url = getToolURL().trim(); if (url === "") { return $.Deferred().resolve(); } var toolButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON); startLoading(toolButton); var promise = toolType.isCartridge(url); promise.always(function() { stopLoading(toolButton); }); promise.done(function(result) { if (result.iscartridge) { $(SELECTORS.TOOL_URL).val(''); $(document).trigger(ltiEvents.START_CARTRIDGE_REGISTRATION, url); } else { $(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION, {url: url}); } }); promise.fail(function() { str.get_string('errorbadurl', 'mod_lti') .done(function(s) { $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, { message: s, error: true }); }) .fail(notification.exception); }); return promise; }; /** * Sets up the listeners for user interaction on the page. * * @method registerEventListeners * @private */ var registerEventListeners = function() { // These are events fired by the registration processes. Either // the cartridge registration or the external registration url. $(document).on(ltiEvents.NEW_TOOL_TYPE, function() { reloadToolList(); }); $(document).on(ltiEvents.START_EXTERNAL_REGISTRATION, function() { showExternalRegistration(); $(SELECTORS.TOOL_URL).val(''); hideToolList(); }); $(document).on(ltiEvents.STOP_EXTERNAL_REGISTRATION, function() { showToolList(); showRegistrationChoices(); }); $(document).on(ltiEvents.START_CARTRIDGE_REGISTRATION, function(event, url) { showCartridgeRegistration(url); }); $(document).on(ltiEvents.STOP_CARTRIDGE_REGISTRATION, function() { getCartridgeRegistrationContainer().find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).removeAttr('data-cartridge-url'); showRegistrationChoices(); }); $(document).on(ltiEvents.REGISTRATION_FEEDBACK, function(event, data) { showRegistrationFeedback(data); }); var addLegacyButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON); addLegacyButton.click(function(e) { e.preventDefault(); addLTILegacyTool(); }); var addLTIButton = $(SELECTORS.TOOL_CREATE_BUTTON); addLTIButton.click(function(e) { e.preventDefault(); addLTIAdvTool(); }); }; return /** @alias module:mod_lti/cartridge_registration_form */ { /** * Initialise this module. */ init: function() { registerEventListeners(); reloadToolList(); } }; }); amd/src/tool_types_and_proxies.js000064400000004030152265520610013227 0ustar00// 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 . /** * Provides an interface for external tools in the Moodle server. * * @module mod_lti/tool_types_and_proxies * @class tool_types_and_proxies * @copyright 2020 Andrew Madden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */ import ajax from 'core/ajax'; /** * Get a list of LTI tool types and tool proxies from Moodle for the given * search args. * * See also: * mod/lti/classes/external.php get_tool_types_and_proxies() * * @method query * @public * @param {Object} args Search parameters * @return {Promise} Promise that will be resolved when the ajax call returns. */ export const query = (args) => { const request = { methodname: 'mod_lti_get_tool_types_and_proxies', args: args || {} }; return ajax.call([request])[0]; }; /** * Get a count of LTI tool types and tool proxies from Moodle for the given * search args. * * See also: * mod/lti/classes/external.php get_tool_types_and_proxies_count() * * @method count * @public * @param {Object} args Search parameters * @return {Promise} Promise that will be resolved when the ajax call returns. */ export const count = (args) => { const request = { methodname: 'mod_lti_get_tool_types_and_proxies_count', args: args || {} }; return ajax.call([request])[0]; }; amd/src/repository.js000064400000002615152265520610010661 0ustar00// 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 . /** * Module to handle AJAX interactions. * * @module mod_lti/repository * @copyright 2023 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; /** * Toggle coursevisible of a tool * * @param {Number} tooltypeid Too type id * @param {Number} courseid Course ID * @param {Number} showinactivitychooser showinactivitychooser state * @return {Promise} */ export const toggleShowInActivityChooser = ( tooltypeid, courseid, showinactivitychooser, ) => Ajax.call([{ methodname: 'mod_lti_toggle_showinactivitychooser', args: { tooltypeid, courseid, showinactivitychooser, }, }])[0]; amd/src/tool_type.js000064400000011270152265520610010455 0ustar00// 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 . /** * Provides an interface for a tool type in the Moodle server. * * @module mod_lti/tool_type * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['core/ajax', 'core/notification'], function(ajax, notification) { return { /** * Get a list of tool types from Moodle for the given * search args. * * See also: * mod/lti/classes/external.php get_tool_types_parameters() * * @method query * @public * @param {Object} args Search parameters * @return {Promise} jQuery Deferred object */ query: function(args) { var request = { methodname: 'mod_lti_get_tool_types', args: args || {} }; var promise = ajax.call([request])[0]; promise.fail(notification.exception); return promise; }, /** * Create a tool type in Moodle. * * The promise will fail if the URL is not a cartridge, so you must handle the fail result. * * See also: * mod/lti/classes/external.php create_tool_type_parameters() * * @method create * @public * @param {Object} args Tool type properties * @return {Promise} jQuery Deferred object */ create: function(args) { var request = { methodname: 'mod_lti_create_tool_type', args: args }; var promise = ajax.call([request])[0]; return promise; }, /** * Update a tool type in Moodle. * * See also: * mod/lti/classes/external.php update_tool_type_parameters() * * @method update * @public * @param {Object} args Tool type properties * @return {Promise} jQuery Deferred object */ update: function(args) { var request = { methodname: 'mod_lti_update_tool_type', args: args }; var promise = ajax.call([request])[0]; promise.fail(notification.exception); return promise; }, /** * Delete a tool type from Moodle. * * @method delete * @public * @param {Integer} id Tool type ID * @return {Promise} jQuery Deferred object */ 'delete': function(id) { var request = { methodname: 'mod_lti_delete_tool_type', args: { id: id } }; var promise = ajax.call([request])[0]; promise.fail(notification.exception); return promise; }, /** * Get a list of tool types from Moodle for the given * tool proxy id. * * @method query * @public * @param {Integer} id Tool type ID * @return {Promise} jQuery Deferred object */ getFromToolProxyId: function(id) { return this.query({toolproxyid: id}); }, /** * Check if the given URL is a cartridge URL. * * The promise will fail if the URL is unreachable, so you must handle the fail result. * * @method isCartridge * @public * @param {String} url * @return {Promise} jQuery Deferred object */ isCartridge: function(url) { var request = { methodname: 'mod_lti_is_cartridge', args: { url: url } }; var promise = ajax.call([request])[0]; return promise; }, /** * Tool type constants. */ constants: { state: { configured: 1, pending: 2, rejected: 3 }, } }; }); amd/src/external_registration_return.js000064400000004062152265520610014453 0ustar00// 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 . /** * Handles the return params from the external registration page after it * redirects back to Moodle. * * See also: mod/lti/externalregistrationreturn.php * * @module mod_lti/external_registration_return * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define([], function() { return { /** * If this was rendered in an iframe then trigger the external registration * complete behaviour in the parent page and provide the params returned from * the external registration page. * * @param {String} message The registration message from the external registration page * @param {String} error The registration error message from the external registration page, if * there was an error. * @param {Integer} id The tool proxy id for the external registration. * @param {String} status Whether the external registration was successful or not. */ init: function(message, error, id, status) { if (window.parent) { window.parent.triggerExternalRegistrationComplete({ message: message, error: error, id: id, status: status }); } } }; }); amd/src/mod_form.js000064400000006065152265520610010247 0ustar00// 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 . /** * Event handlers for the mod_lti mod_form. * * @module mod_lti/mod_form * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; import ContentItem from 'mod_lti/contentitem'; /** * Initialise module. * * @param {int} courseId the course id. * @param {string} toolUrl the external tool url. */ const init = (courseId, toolUrl) => { const contentItemButton = document.querySelector('[name="selectcontent"]'); if (!contentItemButton) { return; } contentItemButton.addEventListener('click', () => { const contentItemUrl = contentItemButton.getAttribute('data-contentitemurl'); const contentItemId = document.querySelector('#hidden_typeid').value; if (contentItemId) { const title = document.querySelector('#id_name').value.trim(); const text = document.querySelector('#id_introeditor').value.trim(); const postData = { id: contentItemId, course: courseId, title: title, text: text }; // The callback below is called after the content item has been returned and processed. ContentItem.init(contentItemUrl, toolUrl, postData, (returnData) => { if (!returnData.multiple) { // The state of the grade checkbox has already been set by processContentItemReturnData() but that // hasn't fired the click/change event required by formslib to show/hide the dependent grade fields. // Fire it now. const allowGrades = document.querySelector('#id_instructorchoiceacceptgrades'); let allowGradesChangeEvent = new Event('change'); allowGrades.dispatchEvent(allowGradesChangeEvent); // If the tool is set to accept grades, make sure "Point" is selected. if (allowGrades.checked) { const gradeType = document.querySelector('#id_grade_modgrade_type'); gradeType.value = "point"; let gradeTypeChangeEvent = new Event('change'); gradeType.dispatchEvent(gradeTypeChangeEvent); } } }); } }); }; export default { init: init }; amd/src/tool_proxy_card_controller.js000064400000022141152265520610014110 0ustar00// 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 . /** * Controls all of the behaviour and interaction with a tool type card. These are * listed on the LTI tool type management page. * * See template: mod_lti/tool_proxy_card * * @module mod_lti/tool_proxy_card_controller * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/tool_proxy', 'mod_lti/events', 'mod_lti/keys', 'core/str'], function($, ajax, notification, templates, toolProxy, ltiEvents, KEYS, str) { var SELECTORS = { DELETE_BUTTON: '.delete', CAPABILITIES_CONTAINER: '.capabilities-container', ACTIVATE_BUTTON: '.tool-card-footer a.activate', }; // Timeout in seconds. var ANNOUNCEMENT_TIMEOUT = 2000; /** * Return the delete button element. * * @method getDeleteButton * @private * @param {JQuery} element jQuery object representing the tool card. * @return {JQuery} jQuery object */ var getDeleteButton = function(element) { return element.find(SELECTORS.DELETE_BUTTON); }; /** * Return the activate button for the type. * * @method getActivateButton * @private * @param {JQuery} element jQuery object representing the tool card. * @return {JQuery} jQuery object */ var getActivateButton = function(element) { return element.find(SELECTORS.ACTIVATE_BUTTON); }; /** * Get the type id. * * @method getTypeId * @private * @param {JQuery} element jQuery object representing the tool card. * @return {String} Type ID */ var getTypeId = function(element) { return element.attr('data-proxy-id'); }; /** * Stop any announcement currently visible on the card. * * @method clearAllAnnouncements * @private * @param {JQuery} element jQuery object representing the tool card. */ var clearAllAnnouncements = function(element) { element.removeClass('announcement loading success fail capabilities'); }; /** * Show the loading announcement. * * @method startLoading * @private * @param {JQuery} element jQuery object representing the tool card. */ var startLoading = function(element) { clearAllAnnouncements(element); element.addClass('announcement loading'); }; /** * Hide the loading announcement. * * @method stopLoading * @private * @param {JQuery} element jQuery object representing the tool card. */ var stopLoading = function(element) { element.removeClass('announcement loading'); }; /** * Show the success announcement. The announcement is only * visible for 2 seconds. * * @method announceSuccess * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var announceSuccess = function(element) { var promise = $.Deferred(); clearAllAnnouncements(element); element.addClass('announcement success'); setTimeout(function() { element.removeClass('announcement success'); promise.resolve(); }, ANNOUNCEMENT_TIMEOUT); return promise; }; /** * Show the failure announcement. The announcement is only * visible for 2 seconds. * * @method announceFailure * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var announceFailure = function(element) { var promise = $.Deferred(); clearAllAnnouncements(element); element.addClass('announcement fail'); setTimeout(function() { element.removeClass('announcement fail'); promise.resolve(); }, ANNOUNCEMENT_TIMEOUT); return promise; }; /** * Delete the tool type from the Moodle server. Triggers a success * or failure announcement depending on the result. * * @method deleteType * @private * @param {JQuery} element jQuery object representing the tool card. * @return {Promise} jQuery Deferred object */ var deleteType = function(element) { var promise = $.Deferred(); var typeId = getTypeId(element); startLoading(element); if (typeId === "") { return $.Deferred().resolve(); } str.get_strings([ { key: 'delete', component: 'mod_lti' }, { key: 'delete_confirmation', component: 'mod_lti' }, { key: 'delete', component: 'mod_lti' }, { key: 'cancel', component: 'core' }, ]) .done(function(strs) { notification.confirm(strs[0], strs[1], strs[2], strs[3], function() { toolProxy.delete(typeId) .done(function() { stopLoading(element); announceSuccess(element) .done(function() { element.remove(); promise.resolve(); }) .fail(notification.exception); }) .fail(function(error) { announceFailure(element); promise.reject(error); }); }, function() { stopLoading(element); promise.resolve(); }); }) .fail(function(error) { stopLoading(element); notification.exception(error); promise.reject(error); }); return promise; }; /** * The user wishes to activate this tool so show them the capabilities that * they need to agree to or if there are none then set the tool type's state * to active. * * @method activateToolType * @private * @param {JQuery} element jQuery object representing the tool card. */ var activateToolType = function(element) { var data = {proxyid: getTypeId(element)}; $(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION, data); }; /** * Sets up the listeners for user interaction on this tool type card. * * @method registerEventListeners * @private * @param {JQuery} element jQuery object representing the tool card. */ var registerEventListeners = function(element) { var deleteButton = getDeleteButton(element); deleteButton.click(function(e) { e.preventDefault(); deleteType(element); }); deleteButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); deleteButton.click(); } } }); var activateButton = getActivateButton(element); activateButton.click(function(e) { e.preventDefault(); activateToolType(element); }); activateButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); activateButton.click(); } } }); }; return /** @alias module:mod_lti/tool_card_controller */ { /** * Initialise this module. * * @param {JQuery} element jQuery object representing the tool card. */ init: function(element) { registerEventListeners(element); } }; }); amd/src/external_registration.js000064400000053660152265520610013064 0ustar00// 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 . /** * Encapsules the behavior for creating a tool type and tool proxy from a * registration url in Moodle. * * Manages the UI while operations are occuring, including rendering external * registration page within the iframe. * * See template: mod_lti/external_registration * * @module mod_lti/external_registration * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/events', 'mod_lti/tool_proxy', 'mod_lti/tool_type', 'mod_lti/keys', 'core/str'], function($, ajax, notification, templates, ltiEvents, toolProxy, toolType, KEYS, str) { var SELECTORS = { EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-page-container', EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER: '#external-registration-template-container', EXTERNAL_REGISTRATION_CANCEL_BUTTON: '#cancel-external-registration', TOOL_TYPE_CAPABILITIES_CONTAINER: '#tool-type-capabilities-container', TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER: '#tool-type-capabilities-template-container', CAPABILITIES_AGREE_CONTAINER: '.capabilities-container', }; /** * Return the external registration cancel button element. This button is * the cancel button that appears while the iframe is rendered. * * @method getExternalRegistrationCancelButton * @private * @return {JQuery} jQuery object */ var getExternalRegistrationCancelButton = function() { return $(SELECTORS.EXTERNAL_REGISTRATION_CANCEL_BUTTON); }; /** * Return the container that holds all elements for the external registration, including * the cancel button and the iframe. * * @method getExternalRegistrationContainer * @private * @return {JQuery} jQuery object */ var getExternalRegistrationContainer = function() { return $(SELECTORS.EXTERNAL_REGISTRATION_CONTAINER); }; /** * Return the container that holds the external registration page template. It should * be the iframe. * * @method getExternalRegistrationTemplateContainer * @private * @return {JQuery} jQuery object */ var getExternalRegistrationTemplateContainer = function() { return $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER); }; /** * Return the container that holds the elements for displaying the list of capabilities * that this tool type requires. This container wraps the loading indicator and the template * container. * * @method getToolTypeCapabilitiesContainer * @private * @return {JQuery} jQuery object */ var getToolTypeCapabilitiesContainer = function() { return $(SELECTORS.TOOL_TYPE_CAPABILITIES_CONTAINER); }; /** * Return the container that holds the template that lists the capabilities that the * tool type will require. * * @method getToolTypeCapabilitiesTemplateContainer * @private * @return {JQuery} jQuery object */ var getToolTypeCapabilitiesTemplateContainer = function() { return $(SELECTORS.TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER); }; /** * Triggers a visual indicator to show that the capabilities section is loading. * * @method startLoadingCapabilitiesContainer * @private */ var startLoadingCapabilitiesContainer = function() { getToolTypeCapabilitiesContainer().addClass('loading'); }; /** * Removes the visual indicator that shows the capabilities section is loading. * * @method stopLoadingCapabilitiesContainer * @private */ var stopLoadingCapabilitiesContainer = function() { getToolTypeCapabilitiesContainer().removeClass('loading'); }; /** * Adds a visual indicator that shows the cancel button is loading. * * @method startLoadingCancel * @private */ var startLoadingCancel = function() { getExternalRegistrationCancelButton().addClass('loading'); }; /** * Adds a visual indicator that shows the cancel button is loading. * * @method startLoadingCancel * @private */ var stopLoadingCancel = function() { getExternalRegistrationCancelButton().removeClass('loading'); }; /** * Stops displaying the tool type capabilities container. * * @method hideToolTypeCapabilitiesContainer * @private */ var hideToolTypeCapabilitiesContainer = function() { getToolTypeCapabilitiesContainer().addClass('hidden'); }; /** * Displays the tool type capabilities container. * * @method showToolTypeCapabilitiesContainer * @private */ var showToolTypeCapabilitiesContainer = function() { getToolTypeCapabilitiesContainer().removeClass('hidden'); }; /** * Stops displaying the external registration content. * * @method hideExternalRegistrationContent * @private */ var hideExternalRegistrationContent = function() { getExternalRegistrationContainer().addClass('hidden'); }; /** * Displays the external registration content. * * @method showExternalRegistrationContent * @private */ var showExternalRegistrationContent = function() { getExternalRegistrationContainer().removeClass('hidden'); }; /** * Save the given tool proxy id on the DOM. * * @method setToolProxyId * @private * @param {Integer} id Tool proxy ID */ var setToolProxyId = function(id) { var button = getExternalRegistrationCancelButton(); button.attr('data-tool-proxy-id', id); }; /** * Return the saved tool proxy id. * * @method getToolProxyId * @private * @return {String} Tool proxy ID */ var getToolProxyId = function() { var button = getExternalRegistrationCancelButton(); return button.attr('data-tool-proxy-id'); }; /** * Remove the saved tool proxy id. * * @method clearToolProxyId * @private */ var clearToolProxyId = function() { var button = getExternalRegistrationCancelButton(); button.removeAttr('data-tool-proxy-id'); }; /** * Returns true if a tool proxy id has been recorded. * * @method hasToolProxyId * @private * @return {Boolean} */ var hasToolProxyId = function() { return getToolProxyId() ? true : false; }; /** * Checks if this process has created a tool proxy within * Moodle yet. * * @method hasCreatedToolProxy * @private * @return {Boolean} */ var hasCreatedToolProxy = function() { var button = getExternalRegistrationCancelButton(); return button.attr('data-tool-proxy-new') && hasToolProxyId(); }; /** * Records that this process has created a tool proxy. * * @method setProxyAsNew * @private * @return {Boolean} */ var setProxyAsNew = function() { var button = getExternalRegistrationCancelButton(); return button.attr('data-tool-proxy-new', "new"); }; /** * Records that this process has not created a tool proxy. * * @method setProxyAsOld * @private * @return {Boolean} */ var setProxyAsOld = function() { var button = getExternalRegistrationCancelButton(); return button.removeAttr('data-tool-proxy-new'); }; /** * Gets the external registration request required to be sent to the external * registration page using a form. * * See mod_lti/tool_proxy_registration_form template. * * @method getRegistrationRequest * @private * @param {Integer} id Tool Proxy ID * @return {Promise} jQuery Deferred object */ var getRegistrationRequest = function(id) { var request = { methodname: 'mod_lti_get_tool_proxy_registration_request', args: { id: id } }; return ajax.call([request])[0]; }; /** * Cancel an in progress external registration. This will perform any necessary * clean up of tool proxies and return the page section back to the home section. * * @method cancelRegistration * @private * @return {Promise} jQuery Deferred object */ var cancelRegistration = function() { startLoadingCancel(); var promise = $.Deferred(); // If we've created a proxy as part of this process then // we need to delete it to clean up the data in the back end. if (hasCreatedToolProxy()) { var id = getToolProxyId(); toolProxy.delete(id).done(function() { promise.resolve(); }).fail(function(failure) { promise.reject(failure); }); } else { promise.resolve(); } promise.done(function() { // Return to the original page. finishExternalRegistration(); stopLoadingCancel(); }).fail(function(failure) { notification.exception(failure); finishExternalRegistration(); stopLoadingCancel(); str.get_string('failedtodeletetoolproxy', 'mod_lti').done(function(s) { var feedback = { message: s, error: true }; $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback); }).fail(notification.exception); }); return promise; }; /** * Load the external registration template and render it in the DOM and display it. * * @method renderExternalRegistrationWindow * @private * @param {Object} registrationRequest * @return {Promise} jQuery Deferred object */ var renderExternalRegistrationWindow = function(registrationRequest) { var promise = templates.render('mod_lti/tool_proxy_registration_form', registrationRequest); promise.done(function(html, js) { // Show the external registration page in an iframe. var container = getExternalRegistrationTemplateContainer(); container.append(html); templates.runTemplateJS(js); container.find('form').submit(); showExternalRegistrationContent(); }).fail(notification.exception); return promise; }; /** * Send a request to Moodle server to set the state of the tool type to configured (active). * * @method setTypeStatusActive * @private * @param {Object} typeData A set of data representing a type, as returned by a request to get a type * from the Moodle server. * @return {Promise} jQuery Deferred object */ var setTypeStatusActive = function(typeData) { return toolType.update({ id: typeData.id, state: toolType.constants.state.configured }); }; /** * Render and display an agreement page for the user to acknowledge the list of capabilities * (groups of data) that the external tool requires in order to work. If the user agrees then * we will activate the tool so that it is immediately available. If they don't agree then * the tool remains in a pending state within Moodle until agreement is given. * * @method promptForToolTypeCapabilitiesAgreement * @private * @param {Object} typeData A set of data representing a type, as returned by a request to get a type * from the Moodle server. * @return {Promise} jQuery Deferred object */ var promptForToolTypeCapabilitiesAgreement = function(typeData) { var promise = $.Deferred(); templates.render('mod_lti/tool_type_capabilities_agree', typeData).done(function(html, js) { var container = getToolTypeCapabilitiesTemplateContainer(); hideExternalRegistrationContent(); showToolTypeCapabilitiesContainer(); templates.replaceNodeContents(container, html, js); var choiceContainer = container.find(SELECTORS.CAPABILITIES_AGREE_CONTAINER); // The user agrees to allow the tool to use the groups of data so we can go // ahead and activate it for them so that it can be used straight away. choiceContainer.on(ltiEvents.CAPABILITIES_AGREE, function() { startLoadingCapabilitiesContainer(); setTypeStatusActive(typeData).always(function() { stopLoadingCapabilitiesContainer(); container.empty(); promise.resolve(); }); }); // The user declines to let the tool use the data. In this case we leave // the tool as pending and they can delete it using the main screen if they // wish. choiceContainer.on(ltiEvents.CAPABILITIES_DECLINE, function() { container.empty(); promise.resolve(); }); }).fail(promise.reject); promise.done(function() { hideToolTypeCapabilitiesContainer(); }).fail(notification.exception); return promise; }; /** * Send a request to the Moodle server to create a tool proxy using the registration URL the user * has provided. The proxy is required for the external registration page to work correctly. * * After the proxy is created the external registration page is rendered within an iframe for the user * to complete the registration in the external page. * * If the tool proxy creation fails then we redirect the page section back to the home section and * display the error, rather than rendering the external registration page. * * @method createAndRegisterToolProxy * @private * @param {String} url Tool registration URL to register * @return {Promise} jQuery Deferred object */ var createAndRegisterToolProxy = function(url) { var promise = $.Deferred(); if (!url || url === "") { // No URL has been input so do nothing. promise.resolve(); } else { // A tool proxy needs to exist before the external page is rendered because // the external page sends requests back to Moodle for information that is stored // in the proxy. toolProxy.create({regurl: url}) .done(function(result) { // Note that it's a new proxy so we will always clean it up. setProxyAsNew(); promise = registerProxy(result.id); }) .fail(function(exception) { // Clean up. cancelRegistration(); // Let the user know what the error is. var feedback = { message: exception.message, error: true }; $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback); promise.reject(exception); }); } return promise; }; /** * Loads the window to register a proxy, given an ID. * * @method registerProxy * @private * @param {Integer} id Proxy id to register * @return {Promise} jQuery Deferred object to fail or resolve */ var registerProxy = function(id) { var promise = $.Deferred(); // Save the id on the DOM to cleanup later. setToolProxyId(id); // There is a specific set of data needed to send to the external registration page // in a form, so let's get it from our server. getRegistrationRequest(id) .done(function(registrationRequest) { renderExternalRegistrationWindow(registrationRequest) .done(function() { promise.resolve(); }) .fail(promise.fail); }) .fail(promise.fail); return promise; }; /** * Complete the registration process, clean up any left over data and * trigger the appropriate events. * * @method finishExternalRegistration * @private */ var finishExternalRegistration = function() { if (hasToolProxyId()) { clearToolProxyId(); } setProxyAsOld(false); hideExternalRegistrationContent(); var container = getExternalRegistrationTemplateContainer(); container.empty(); $(document).trigger(ltiEvents.STOP_EXTERNAL_REGISTRATION); }; /** * Sets up the listeners for user interaction on the page. * * @method registerEventListeners * @private */ var registerEventListeners = function() { $(document).on(ltiEvents.START_EXTERNAL_REGISTRATION, function(event, data) { if (!data) { return; } if (data.url) { createAndRegisterToolProxy(data.url); } if (data.proxyid) { registerProxy(data.proxyid); } }); var cancelExternalRegistrationButton = getExternalRegistrationCancelButton(); cancelExternalRegistrationButton.click(function(e) { e.preventDefault(); cancelRegistration(); }); cancelExternalRegistrationButton.keypress(function(e) { if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) { if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) { e.preventDefault(); cancelRegistration(); } } }); // This is gross but necessary due to isolated jQuery scopes between // child iframe and parent windows. There is no other way to communicate. // // This function gets called by the moodle page that received the redirect // from the external registration page and handles the external page's returned // parameters. // // See AMD module mod_lti/external_registration_return. window.triggerExternalRegistrationComplete = function(data) { var promise = $.Deferred(); var feedback = { message: "", error: false }; if (data.status == "success") { str.get_string('successfullycreatedtooltype', 'mod_lti').done(function(s) { feedback.message = s; }).fail(notification.exception); // Trigger appropriate events when we've completed the necessary requests. promise.done(function() { finishExternalRegistration(); $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback); $(document).trigger(ltiEvents.NEW_TOOL_TYPE); }).fail(notification.exception); // We should have created a tool proxy by this point. if (hasCreatedToolProxy()) { var proxyId = getToolProxyId(); // We need the list of types that are linked to this proxy. We're assuming it'll // only be one because this process creates a one-to-one type->proxy. toolType.getFromToolProxyId(proxyId).done(function(types) { if (types && types.length) { // There should only be one result. var typeData = types[0]; // Check if the external tool required access to any Moodle data (users, courses etc). if (typeData.hascapabilitygroups) { // If it did then we ask the user to agree to those groups before the type is // activated (i.e. can be used in Moodle). promptForToolTypeCapabilitiesAgreement(typeData).always(function() { promise.resolve(); }); } else { promise.resolve(); } } else { promise.resolve(); } }).fail(function() { promise.resolve(); }); } } else { // Anything other than success is failure. feedback.message = data.error; feedback.error = true; // Cancel registration to clean up any proxies and tools that were // created. promise.done(function() { cancelRegistration().always(function() { $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback); }); }).fail(notification.exception); promise.resolve(); } return promise; }; }; return { /** * Initialise this module. */ init: function() { registerEventListeners(); } }; }); amd/build/external_registration.min.js.map000064400000067261152265520610014734 0ustar00{"version":3,"file":"external_registration.min.js","sources":["../src/external_registration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Encapsules the behavior for creating a tool type and tool proxy from a\n * registration url in Moodle.\n *\n * Manages the UI while operations are occuring, including rendering external\n * registration page within the iframe.\n *\n * See template: mod_lti/external_registration\n *\n * @module mod_lti/external_registration\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/events',\n 'mod_lti/tool_proxy', 'mod_lti/tool_type', 'mod_lti/keys', 'core/str'],\n function($, ajax, notification, templates, ltiEvents, toolProxy, toolType, KEYS, str) {\n\n var SELECTORS = {\n EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-page-container',\n EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER: '#external-registration-template-container',\n EXTERNAL_REGISTRATION_CANCEL_BUTTON: '#cancel-external-registration',\n TOOL_TYPE_CAPABILITIES_CONTAINER: '#tool-type-capabilities-container',\n TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER: '#tool-type-capabilities-template-container',\n CAPABILITIES_AGREE_CONTAINER: '.capabilities-container',\n };\n\n /**\n * Return the external registration cancel button element. This button is\n * the cancel button that appears while the iframe is rendered.\n *\n * @method getExternalRegistrationCancelButton\n * @private\n * @return {JQuery} jQuery object\n */\n var getExternalRegistrationCancelButton = function() {\n return $(SELECTORS.EXTERNAL_REGISTRATION_CANCEL_BUTTON);\n };\n\n /**\n * Return the container that holds all elements for the external registration, including\n * the cancel button and the iframe.\n *\n * @method getExternalRegistrationContainer\n * @private\n * @return {JQuery} jQuery object\n */\n var getExternalRegistrationContainer = function() {\n return $(SELECTORS.EXTERNAL_REGISTRATION_CONTAINER);\n };\n\n /**\n * Return the container that holds the external registration page template. It should\n * be the iframe.\n *\n * @method getExternalRegistrationTemplateContainer\n * @private\n * @return {JQuery} jQuery object\n */\n var getExternalRegistrationTemplateContainer = function() {\n return $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);\n };\n\n /**\n * Return the container that holds the elements for displaying the list of capabilities\n * that this tool type requires. This container wraps the loading indicator and the template\n * container.\n *\n * @method getToolTypeCapabilitiesContainer\n * @private\n * @return {JQuery} jQuery object\n */\n var getToolTypeCapabilitiesContainer = function() {\n return $(SELECTORS.TOOL_TYPE_CAPABILITIES_CONTAINER);\n };\n\n /**\n * Return the container that holds the template that lists the capabilities that the\n * tool type will require.\n *\n * @method getToolTypeCapabilitiesTemplateContainer\n * @private\n * @return {JQuery} jQuery object\n */\n var getToolTypeCapabilitiesTemplateContainer = function() {\n return $(SELECTORS.TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER);\n };\n\n /**\n * Triggers a visual indicator to show that the capabilities section is loading.\n *\n * @method startLoadingCapabilitiesContainer\n * @private\n */\n var startLoadingCapabilitiesContainer = function() {\n getToolTypeCapabilitiesContainer().addClass('loading');\n };\n\n /**\n * Removes the visual indicator that shows the capabilities section is loading.\n *\n * @method stopLoadingCapabilitiesContainer\n * @private\n */\n var stopLoadingCapabilitiesContainer = function() {\n getToolTypeCapabilitiesContainer().removeClass('loading');\n };\n\n /**\n * Adds a visual indicator that shows the cancel button is loading.\n *\n * @method startLoadingCancel\n * @private\n */\n var startLoadingCancel = function() {\n getExternalRegistrationCancelButton().addClass('loading');\n };\n\n /**\n * Adds a visual indicator that shows the cancel button is loading.\n *\n * @method startLoadingCancel\n * @private\n */\n var stopLoadingCancel = function() {\n getExternalRegistrationCancelButton().removeClass('loading');\n };\n\n /**\n * Stops displaying the tool type capabilities container.\n *\n * @method hideToolTypeCapabilitiesContainer\n * @private\n */\n var hideToolTypeCapabilitiesContainer = function() {\n getToolTypeCapabilitiesContainer().addClass('hidden');\n };\n\n /**\n * Displays the tool type capabilities container.\n *\n * @method showToolTypeCapabilitiesContainer\n * @private\n */\n var showToolTypeCapabilitiesContainer = function() {\n getToolTypeCapabilitiesContainer().removeClass('hidden');\n };\n\n /**\n * Stops displaying the external registration content.\n *\n * @method hideExternalRegistrationContent\n * @private\n */\n var hideExternalRegistrationContent = function() {\n getExternalRegistrationContainer().addClass('hidden');\n };\n\n /**\n * Displays the external registration content.\n *\n * @method showExternalRegistrationContent\n * @private\n */\n var showExternalRegistrationContent = function() {\n getExternalRegistrationContainer().removeClass('hidden');\n };\n\n /**\n * Save the given tool proxy id on the DOM.\n *\n * @method setToolProxyId\n * @private\n * @param {Integer} id Tool proxy ID\n */\n var setToolProxyId = function(id) {\n var button = getExternalRegistrationCancelButton();\n button.attr('data-tool-proxy-id', id);\n };\n\n /**\n * Return the saved tool proxy id.\n *\n * @method getToolProxyId\n * @private\n * @return {String} Tool proxy ID\n */\n var getToolProxyId = function() {\n var button = getExternalRegistrationCancelButton();\n return button.attr('data-tool-proxy-id');\n };\n\n /**\n * Remove the saved tool proxy id.\n *\n * @method clearToolProxyId\n * @private\n */\n var clearToolProxyId = function() {\n var button = getExternalRegistrationCancelButton();\n button.removeAttr('data-tool-proxy-id');\n };\n\n /**\n * Returns true if a tool proxy id has been recorded.\n *\n * @method hasToolProxyId\n * @private\n * @return {Boolean}\n */\n var hasToolProxyId = function() {\n return getToolProxyId() ? true : false;\n };\n\n /**\n * Checks if this process has created a tool proxy within\n * Moodle yet.\n *\n * @method hasCreatedToolProxy\n * @private\n * @return {Boolean}\n */\n var hasCreatedToolProxy = function() {\n var button = getExternalRegistrationCancelButton();\n return button.attr('data-tool-proxy-new') && hasToolProxyId();\n };\n\n /**\n * Records that this process has created a tool proxy.\n *\n * @method setProxyAsNew\n * @private\n * @return {Boolean}\n */\n var setProxyAsNew = function() {\n var button = getExternalRegistrationCancelButton();\n return button.attr('data-tool-proxy-new', \"new\");\n };\n\n /**\n * Records that this process has not created a tool proxy.\n *\n * @method setProxyAsOld\n * @private\n * @return {Boolean}\n */\n var setProxyAsOld = function() {\n var button = getExternalRegistrationCancelButton();\n return button.removeAttr('data-tool-proxy-new');\n };\n\n /**\n * Gets the external registration request required to be sent to the external\n * registration page using a form.\n *\n * See mod_lti/tool_proxy_registration_form template.\n *\n * @method getRegistrationRequest\n * @private\n * @param {Integer} id Tool Proxy ID\n * @return {Promise} jQuery Deferred object\n */\n var getRegistrationRequest = function(id) {\n var request = {\n methodname: 'mod_lti_get_tool_proxy_registration_request',\n args: {\n id: id\n }\n };\n\n return ajax.call([request])[0];\n };\n\n /**\n * Cancel an in progress external registration. This will perform any necessary\n * clean up of tool proxies and return the page section back to the home section.\n *\n * @method cancelRegistration\n * @private\n * @return {Promise} jQuery Deferred object\n */\n var cancelRegistration = function() {\n startLoadingCancel();\n var promise = $.Deferred();\n\n // If we've created a proxy as part of this process then\n // we need to delete it to clean up the data in the back end.\n if (hasCreatedToolProxy()) {\n var id = getToolProxyId();\n toolProxy.delete(id).done(function() {\n promise.resolve();\n }).fail(function(failure) {\n promise.reject(failure);\n });\n } else {\n promise.resolve();\n }\n\n promise.done(function() {\n // Return to the original page.\n finishExternalRegistration();\n stopLoadingCancel();\n }).fail(function(failure) {\n notification.exception(failure);\n finishExternalRegistration();\n stopLoadingCancel();\n str.get_string('failedtodeletetoolproxy', 'mod_lti').done(function(s) {\n var feedback = {\n message: s,\n error: true\n };\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback);\n }).fail(notification.exception);\n });\n\n return promise;\n };\n\n /**\n * Load the external registration template and render it in the DOM and display it.\n *\n * @method renderExternalRegistrationWindow\n * @private\n * @param {Object} registrationRequest\n * @return {Promise} jQuery Deferred object\n */\n var renderExternalRegistrationWindow = function(registrationRequest) {\n var promise = templates.render('mod_lti/tool_proxy_registration_form', registrationRequest);\n\n promise.done(function(html, js) {\n // Show the external registration page in an iframe.\n var container = getExternalRegistrationTemplateContainer();\n container.append(html);\n templates.runTemplateJS(js);\n\n container.find('form').submit();\n showExternalRegistrationContent();\n }).fail(notification.exception);\n\n return promise;\n };\n\n /**\n * Send a request to Moodle server to set the state of the tool type to configured (active).\n *\n * @method setTypeStatusActive\n * @private\n * @param {Object} typeData A set of data representing a type, as returned by a request to get a type\n * from the Moodle server.\n * @return {Promise} jQuery Deferred object\n */\n var setTypeStatusActive = function(typeData) {\n return toolType.update({\n id: typeData.id,\n state: toolType.constants.state.configured\n });\n };\n\n /**\n * Render and display an agreement page for the user to acknowledge the list of capabilities\n * (groups of data) that the external tool requires in order to work. If the user agrees then\n * we will activate the tool so that it is immediately available. If they don't agree then\n * the tool remains in a pending state within Moodle until agreement is given.\n *\n * @method promptForToolTypeCapabilitiesAgreement\n * @private\n * @param {Object} typeData A set of data representing a type, as returned by a request to get a type\n * from the Moodle server.\n * @return {Promise} jQuery Deferred object\n */\n var promptForToolTypeCapabilitiesAgreement = function(typeData) {\n var promise = $.Deferred();\n\n templates.render('mod_lti/tool_type_capabilities_agree', typeData).done(function(html, js) {\n var container = getToolTypeCapabilitiesTemplateContainer();\n\n hideExternalRegistrationContent();\n showToolTypeCapabilitiesContainer();\n\n templates.replaceNodeContents(container, html, js);\n\n var choiceContainer = container.find(SELECTORS.CAPABILITIES_AGREE_CONTAINER);\n\n // The user agrees to allow the tool to use the groups of data so we can go\n // ahead and activate it for them so that it can be used straight away.\n choiceContainer.on(ltiEvents.CAPABILITIES_AGREE, function() {\n startLoadingCapabilitiesContainer();\n setTypeStatusActive(typeData).always(function() {\n stopLoadingCapabilitiesContainer();\n container.empty();\n promise.resolve();\n });\n });\n\n // The user declines to let the tool use the data. In this case we leave\n // the tool as pending and they can delete it using the main screen if they\n // wish.\n choiceContainer.on(ltiEvents.CAPABILITIES_DECLINE, function() {\n container.empty();\n promise.resolve();\n });\n }).fail(promise.reject);\n\n promise.done(function() {\n hideToolTypeCapabilitiesContainer();\n }).fail(notification.exception);\n\n return promise;\n };\n\n /**\n * Send a request to the Moodle server to create a tool proxy using the registration URL the user\n * has provided. The proxy is required for the external registration page to work correctly.\n *\n * After the proxy is created the external registration page is rendered within an iframe for the user\n * to complete the registration in the external page.\n *\n * If the tool proxy creation fails then we redirect the page section back to the home section and\n * display the error, rather than rendering the external registration page.\n *\n * @method createAndRegisterToolProxy\n * @private\n * @param {String} url Tool registration URL to register\n * @return {Promise} jQuery Deferred object\n */\n var createAndRegisterToolProxy = function(url) {\n var promise = $.Deferred();\n\n if (!url || url === \"\") {\n // No URL has been input so do nothing.\n promise.resolve();\n } else {\n // A tool proxy needs to exist before the external page is rendered because\n // the external page sends requests back to Moodle for information that is stored\n // in the proxy.\n toolProxy.create({regurl: url})\n .done(function(result) {\n // Note that it's a new proxy so we will always clean it up.\n setProxyAsNew();\n promise = registerProxy(result.id);\n })\n .fail(function(exception) {\n // Clean up.\n cancelRegistration();\n // Let the user know what the error is.\n var feedback = {\n message: exception.message,\n error: true\n };\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback);\n promise.reject(exception);\n });\n }\n\n return promise;\n };\n\n /**\n * Loads the window to register a proxy, given an ID.\n *\n * @method registerProxy\n * @private\n * @param {Integer} id Proxy id to register\n * @return {Promise} jQuery Deferred object to fail or resolve\n */\n var registerProxy = function(id) {\n var promise = $.Deferred();\n // Save the id on the DOM to cleanup later.\n setToolProxyId(id);\n\n // There is a specific set of data needed to send to the external registration page\n // in a form, so let's get it from our server.\n getRegistrationRequest(id)\n .done(function(registrationRequest) {\n renderExternalRegistrationWindow(registrationRequest)\n .done(function() {\n promise.resolve();\n })\n .fail(promise.fail);\n })\n .fail(promise.fail);\n\n return promise;\n };\n\n /**\n * Complete the registration process, clean up any left over data and\n * trigger the appropriate events.\n *\n * @method finishExternalRegistration\n * @private\n */\n var finishExternalRegistration = function() {\n if (hasToolProxyId()) {\n clearToolProxyId();\n }\n setProxyAsOld(false);\n\n hideExternalRegistrationContent();\n var container = getExternalRegistrationTemplateContainer();\n container.empty();\n\n $(document).trigger(ltiEvents.STOP_EXTERNAL_REGISTRATION);\n };\n\n /**\n * Sets up the listeners for user interaction on the page.\n *\n * @method registerEventListeners\n * @private\n */\n var registerEventListeners = function() {\n\n $(document).on(ltiEvents.START_EXTERNAL_REGISTRATION, function(event, data) {\n if (!data) {\n return;\n }\n if (data.url) {\n createAndRegisterToolProxy(data.url);\n }\n if (data.proxyid) {\n registerProxy(data.proxyid);\n }\n });\n\n var cancelExternalRegistrationButton = getExternalRegistrationCancelButton();\n cancelExternalRegistrationButton.click(function(e) {\n e.preventDefault();\n cancelRegistration();\n });\n cancelExternalRegistrationButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n cancelRegistration();\n }\n }\n });\n\n // This is gross but necessary due to isolated jQuery scopes between\n // child iframe and parent windows. There is no other way to communicate.\n //\n // This function gets called by the moodle page that received the redirect\n // from the external registration page and handles the external page's returned\n // parameters.\n //\n // See AMD module mod_lti/external_registration_return.\n window.triggerExternalRegistrationComplete = function(data) {\n var promise = $.Deferred();\n var feedback = {\n message: \"\",\n error: false\n };\n\n if (data.status == \"success\") {\n str.get_string('successfullycreatedtooltype', 'mod_lti').done(function(s) {\n feedback.message = s;\n }).fail(notification.exception);\n\n // Trigger appropriate events when we've completed the necessary requests.\n promise.done(function() {\n finishExternalRegistration();\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback);\n $(document).trigger(ltiEvents.NEW_TOOL_TYPE);\n }).fail(notification.exception);\n\n // We should have created a tool proxy by this point.\n if (hasCreatedToolProxy()) {\n var proxyId = getToolProxyId();\n\n // We need the list of types that are linked to this proxy. We're assuming it'll\n // only be one because this process creates a one-to-one type->proxy.\n toolType.getFromToolProxyId(proxyId).done(function(types) {\n if (types && types.length) {\n // There should only be one result.\n var typeData = types[0];\n\n // Check if the external tool required access to any Moodle data (users, courses etc).\n if (typeData.hascapabilitygroups) {\n // If it did then we ask the user to agree to those groups before the type is\n // activated (i.e. can be used in Moodle).\n promptForToolTypeCapabilitiesAgreement(typeData).always(function() {\n promise.resolve();\n });\n } else {\n promise.resolve();\n }\n } else {\n promise.resolve();\n }\n }).fail(function() {\n promise.resolve();\n });\n }\n } else {\n // Anything other than success is failure.\n feedback.message = data.error;\n feedback.error = true;\n\n // Cancel registration to clean up any proxies and tools that were\n // created.\n promise.done(function() {\n cancelRegistration().always(function() {\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, feedback);\n });\n }).fail(notification.exception);\n\n promise.resolve();\n }\n\n return promise;\n };\n };\n\n return {\n\n /**\n * Initialise this module.\n */\n init: function() {\n registerEventListeners();\n }\n };\n});\n"],"names":["define","$","ajax","notification","templates","ltiEvents","toolProxy","toolType","KEYS","str","SELECTORS","getExternalRegistrationCancelButton","getExternalRegistrationContainer","getExternalRegistrationTemplateContainer","getToolTypeCapabilitiesContainer","stopLoadingCancel","removeClass","hideExternalRegistrationContent","addClass","getToolProxyId","attr","hasToolProxyId","hasCreatedToolProxy","cancelRegistration","promise","Deferred","id","delete","done","resolve","fail","failure","reject","finishExternalRegistration","exception","get_string","s","feedback","message","error","document","trigger","REGISTRATION_FEEDBACK","renderExternalRegistrationWindow","registrationRequest","render","html","js","container","append","runTemplateJS","find","submit","promptForToolTypeCapabilitiesAgreement","typeData","replaceNodeContents","choiceContainer","on","CAPABILITIES_AGREE","update","state","constants","configured","setTypeStatusActive","always","empty","CAPABILITIES_DECLINE","createAndRegisterToolProxy","url","create","regurl","result","registerProxy","setToolProxyId","request","methodname","args","call","getRegistrationRequest","removeAttr","STOP_EXTERNAL_REGISTRATION","init","START_EXTERNAL_REGISTRATION","event","data","proxyid","cancelExternalRegistrationButton","click","e","preventDefault","keypress","metaKey","shiftKey","altKey","ctrlKey","keyCode","ENTER","SPACE","window","triggerExternalRegistrationComplete","status","NEW_TOOL_TYPE","proxyId","getFromToolProxyId","types","length","hascapabilitygroups","registerEventListeners"],"mappings":";;;;;;;;;;;;;;AA6BAA,uCAAO,CAAC,SAAU,YAAa,oBAAqB,iBAAkB,iBAC9D,qBAAsB,oBAAqB,eAAgB,aAC3D,SAASC,EAAGC,KAAMC,aAAcC,UAAWC,UAAWC,UAAWC,SAAUC,KAAMC,SAEjFC,0CACiC,wCADjCA,mDAE0C,4CAF1CA,8CAGqC,gCAHrCA,2CAIkC,oCAJlCA,oDAK2C,6CAL3CA,uCAM8B,0BAW9BC,oCAAsC,kBAC/BV,EAAES,gDAWTE,iCAAmC,kBAC5BX,EAAES,4CAWTG,yCAA2C,kBACpCZ,EAAES,qDAYTI,iCAAmC,kBAC5Bb,EAAES,6CAmDTK,kBAAoB,WACpBJ,sCAAsCK,YAAY,YA6BlDC,gCAAkC,WAClCL,mCAAmCM,SAAS,WAgC5CC,eAAiB,kBACJR,sCACCS,KAAK,uBAqBnBC,eAAiB,mBACVF,kBAWPG,oBAAsB,kBACTX,sCACCS,KAAK,wBAA0BC,kBAyD7CE,mBAAqB,WAtKrBZ,sCAAsCO,SAAS,eAwK3CM,QAAUvB,EAAEwB,cAIZH,sBAAuB,KACnBI,GAAKP,iBACTb,UAAUqB,OAAOD,IAAIE,MAAK,WACtBJ,QAAQK,aACTC,MAAK,SAASC,SACbP,QAAQQ,OAAOD,iBAGnBP,QAAQK,iBAGZL,QAAQI,MAAK,WAETK,6BACAlB,uBACDe,MAAK,SAASC,SACb5B,aAAa+B,UAAUH,SACvBE,6BACAlB,oBACAN,IAAI0B,WAAW,0BAA2B,WAAWP,MAAK,SAASQ,OAC3DC,SAAW,CACXC,QAASF,EACTG,OAAO,GAEXtC,EAAEuC,UAAUC,QAAQpC,UAAUqC,sBAAuBL,aACtDP,KAAK3B,aAAa+B,cAGlBV,SAWPmB,iCAAmC,SAASC,yBACxCpB,QAAUpB,UAAUyC,OAAO,uCAAwCD,4BAEvEpB,QAAQI,MAAK,SAASkB,KAAMC,QAEpBC,UAAYnC,2CAChBmC,UAAUC,OAAOH,MACjB1C,UAAU8C,cAAcH,IAExBC,UAAUG,KAAK,QAAQC,SA1K3BxC,mCAAmCI,YAAY,aA4K5Cc,KAAK3B,aAAa+B,WAEdV,SA+BP6B,uCAAyC,SAASC,cAC9C9B,QAAUvB,EAAEwB,kBAEhBrB,UAAUyC,OAAO,uCAAwCS,UAAU1B,MAAK,SAASkB,KAAMC,QAC/EC,UAjSD/C,EAAES,qDAmSLO,kCAvOJH,mCAAmCE,YAAY,UA0O3CZ,UAAUmD,oBAAoBP,UAAWF,KAAMC,QAE3CS,gBAAkBR,UAAUG,KAAKzC,wCAIrC8C,gBAAgBC,GAAGpD,UAAUqD,oBAAoB,WAlSrD5C,mCAAmCI,SAAS,WAgQtB,SAASoC,iBACxB/C,SAASoD,OAAO,CACnBjC,GAAI4B,SAAS5B,GACbkC,MAAOrD,SAASsD,UAAUD,MAAME,aAiC5BC,CAAoBT,UAAUU,QAAO,WA1R7ClD,mCAAmCE,YAAY,WA4RnCgC,UAAUiB,QACVzC,QAAQK,gBAOhB2B,gBAAgBC,GAAGpD,UAAU6D,sBAAsB,WAC/ClB,UAAUiB,QACVzC,QAAQK,gBAEbC,KAAKN,QAAQQ,QAEhBR,QAAQI,MAAK,WA5Qbd,mCAAmCI,SAAS,aA8QzCY,KAAK3B,aAAa+B,WAEdV,SAkBP2C,2BAA6B,SAASC,SAClC5C,QAAUvB,EAAEwB,kBAEX2C,KAAe,KAARA,IAOR9D,UAAU+D,OAAO,CAACC,OAAQF,MACrBxC,MAAK,SAAS2C,QAzMV5D,sCACCS,KAAK,sBAAuB,OA2M1BI,QAAUgD,cAAcD,OAAO7C,OAEtCI,MAAK,SAASI,WAEPX,yBAEIc,SAAW,CACXC,QAASJ,UAAUI,QACnBC,OAAO,GAEXtC,EAAEuC,UAAUC,QAAQpC,UAAUqC,sBAAuBL,UACrDb,QAAQQ,OAAOE,cApB3BV,QAAQK,UAwBLL,SAWPgD,cAAgB,SAAS9C,QACrBF,QAAUvB,EAAEwB,kBAnSC,SAASC,IACbf,sCACNS,KAAK,qBAAsBM,IAmSlC+C,CAAe/C,IA9MU,SAASA,QAC9BgD,QAAU,CACVC,WAAY,8CACZC,KAAM,CACFlD,GAAIA,YAILxB,KAAK2E,KAAK,CAACH,UAAU,GA0M5BI,CAAuBpD,IAClBE,MAAK,SAASgB,qBACPD,iCAAiCC,qBAC5BhB,MAAK,WACEJ,QAAQK,aAEfC,KAAKN,QAAQM,SAEzBA,KAAKN,QAAQM,MAEXN,SAUPS,2BAA6B,WACzBZ,kBAtSSV,sCACNoE,WAAW,sBA+CLpE,sCACCoE,WAAW,uBA0PzB9D,kCACgBJ,2CACNoD,QAEVhE,EAAEuC,UAAUC,QAAQpC,UAAU2E,mCAgH3B,CAKHC,KAAM,YA5GmB,WAEzBhF,EAAEuC,UAAUiB,GAAGpD,UAAU6E,6BAA6B,SAASC,MAAOC,MACzDA,OAGDA,KAAKhB,KACLD,2BAA2BiB,KAAKhB,KAEhCgB,KAAKC,SACLb,cAAcY,KAAKC,iBAI3BC,iCAAmC3E,sCACvC2E,iCAAiCC,OAAM,SAASC,GAC5CA,EAAEC,iBACFlE,wBAEJ+D,iCAAiCI,UAAS,SAASF,GAC1CA,EAAEG,SAAYH,EAAEI,UAAaJ,EAAEK,QAAWL,EAAEM,SACzCN,EAAEO,SAAWvF,KAAKwF,OAASR,EAAEO,SAAWvF,KAAKyF,QAC7CT,EAAEC,iBACFlE,yBAaZ2E,OAAOC,oCAAsC,SAASf,UAC9C5D,QAAUvB,EAAEwB,WACZY,SAAW,CACXC,QAAS,GACTC,OAAO,MAGQ,WAAf6C,KAAKgB,WACL3F,IAAI0B,WAAW,8BAA+B,WAAWP,MAAK,SAASQ,GACnEC,SAASC,QAAUF,KACpBN,KAAK3B,aAAa+B,WAGrBV,QAAQI,MAAK,WACTK,6BACAhC,EAAEuC,UAAUC,QAAQpC,UAAUqC,sBAAuBL,UACrDpC,EAAEuC,UAAUC,QAAQpC,UAAUgG,kBAC/BvE,KAAK3B,aAAa+B,WAGjBZ,sBAAuB,KACnBgF,QAAUnF,iBAIdZ,SAASgG,mBAAmBD,SAAS1E,MAAK,SAAS4E,UAC3CA,OAASA,MAAMC,OAAQ,KAEnBnD,SAAWkD,MAAM,GAGjBlD,SAASoD,oBAGTrD,uCAAuCC,UAAUU,QAAO,WACpDxC,QAAQK,aAGZL,QAAQK,eAGZL,QAAQK,aAEbC,MAAK,WACJN,QAAQK,mBAKhBQ,SAASC,QAAU8C,KAAK7C,MACxBF,SAASE,OAAQ,EAIjBf,QAAQI,MAAK,WACTL,qBAAqByC,QAAO,WACxB/D,EAAEuC,UAAUC,QAAQpC,UAAUqC,sBAAuBL,gBAE1DP,KAAK3B,aAAa+B,WAErBV,QAAQK,iBAGLL,SAUPmF"}amd/build/repository.min.js000064400000001375152265520610011755 0ustar00define("mod_lti/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * Module to handle AJAX interactions. * * @module mod_lti/repository * @copyright 2023 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.toggleShowInActivityChooser=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.toggleShowInActivityChooser=(tooltypeid,courseid,showinactivitychooser)=>_ajax.default.call([{methodname:"mod_lti_toggle_showinactivitychooser",args:{tooltypeid:tooltypeid,courseid:courseid,showinactivitychooser:showinactivitychooser}}])[0]})); //# sourceMappingURL=repository.min.js.mapamd/build/contentitem.min.js000064400000011507152265520610012065 0ustar00/** * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an * LTI tool provider that supports Content-Item type message. * * See template: mod_lti/contentitem * * @module mod_lti/contentitem * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define("mod_lti/contentitem",["jquery","core/notification","core/str","core/templates","mod_lti/form-field","core/modal","core/modal_events"],(function($,notification,str,templates,FormField,Modal,ModalEvents){var dialogue,doneCallback,contentItem={init:function(url,toolUrl,postData,cb){doneCallback=cb;var context={url:url,toolUrl:toolUrl,postData:postData},bodyPromise=templates.render("mod_lti/contentitem",context);if(dialogue)return dialogue.setBody(bodyPromise),void dialogue.show();str.get_string("selectcontent","lti").then((function(title){return Modal.create({title:title,body:bodyPromise,large:!0,show:!0})})).then((function(modal){dialogue=modal,modal.getRoot().on(ModalEvents.hidden,(function(){modal.setBody(""),notification.fetchNotifications()}))})).catch(notification.exception)}},ltiFormFields=[new FormField("name",FormField.TYPES.TEXT,!1,""),new FormField("introeditor",FormField.TYPES.EDITOR,!1,""),new FormField("toolurl",FormField.TYPES.TEXT,!0,""),new FormField("securetoolurl",FormField.TYPES.TEXT,!0,""),new FormField("instructorchoiceacceptgrades",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendname",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorchoicesendemailaddr",FormField.TYPES.CHECKBOX,!0,!0),new FormField("instructorcustomparameters",FormField.TYPES.TEXT,!0,""),new FormField("icon",FormField.TYPES.TEXT,!0,""),new FormField("secureicon",FormField.TYPES.TEXT,!0,""),new FormField("launchcontainer",FormField.TYPES.SELECT,!0,0),new FormField("grade_modgrade_point",FormField.TYPES.TEXT,!1,""),new FormField("lineitemresourceid",FormField.TYPES.TEXT,!0,""),new FormField("lineitemtag",FormField.TYPES.TEXT,!0,""),new FormField("lineitemsubreviewurl",FormField.TYPES.TEXT,!0,""),new FormField("lineitemsubreviewparams",FormField.TYPES.TEXT,!0,"")];const hideElement=e=>{e.setAttribute("hidden","true"),e.setAttribute("aria-hidden","true"),e.setAttribute("tab-index","-1")},showElement=e=>{e.removeAttribute("hidden"),e.setAttribute("aria-hidden","false"),e.setAttribute("tab-index","1")};return window.processContentItemReturnData=function(returnData){var index;if(dialogue&&dialogue.hide(),returnData.multiple){for(index in ltiFormFields)ltiFormFields[index].setFieldValue("name"===ltiFormFields[index].name?"item":null);var variants=[];returnData.multiple.forEach((function(v){variants.push((config=>{const variant={};return["name","toolurl","securetoolurl","instructorcustomparameters","icon","secureicon","launchcontainer","lineitemresourceid","lineitemtag","lineitemsubreviewurl","lineitemsubreviewparams"].forEach((function(name){variant[name]=config[name]||""})),variant["introeditor[text]"]=config.introeditor?config.introeditor.text:"",variant["introeditor[format]"]=config.introeditor?config.introeditor.format:"",1===config.instructorchoiceacceptgrades?(variant.instructorchoiceacceptgrades="1",variant["grade[modgrade_point]"]=config.grade_modgrade_point||"100"):variant.instructorchoiceacceptgrades="0",variant})(v))})),async function(items){const form=document.querySelector("#region-main-box form"),toolArea=form.querySelector('[data-attribute="dynamic-import"]'),buttonGroup=form.querySelector("#fgroup_id_buttonar"),submitAndLaunch=form.querySelector("#id_submitbutton");Array.from(form.children).forEach(hideElement),hideElement(submitAndLaunch);const{html:html,js:js}=await templates.renderForPromise("mod_lti/tool_deeplinking_results",{items:items});await templates.replaceNodeContents(toolArea,html,js),showElement(toolArea),showElement(buttonGroup)}(returnData.multiple);const submitAndCourse=document.querySelector("#id_submitbutton2");submitAndCourse.onclick=e=>{e.preventDefault(),submitAndCourse.disabled=!0;const fd=new FormData(document.querySelector("#region-main-box form")),backToCourse=()=>{document.querySelector("#id_cancel").click()};variants.reduce(((promise,variant)=>{Object.entries(variant).forEach((entry=>fd.set(entry[0],entry[1])));const body=new URLSearchParams(fd),doPost=()=>fetch(document.location.pathname,{method:"post",body:body});return promise.then(doPost).catch(doPost)}),Promise.resolve()).then(backToCourse).catch(backToCourse)}}else{for(index in ltiFormFields){var field=ltiFormFields[index],value=null;void 0!==returnData[field.name]&&(value=returnData[field.name]),field.setFieldValue(value)}field.setFieldValue(value),document.querySelector("#id_selectcontentindicator").innerHTML=returnData.selectcontentindicator}doneCallback&&doneCallback(returnData)},contentItem})); //# sourceMappingURL=contentitem.min.js.mapamd/build/tool_type.min.js.map000064400000013612152265520610012325 0ustar00{"version":3,"file":"tool_type.min.js","sources":["../src/tool_type.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Provides an interface for a tool type in the Moodle server.\n *\n * @module mod_lti/tool_type\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['core/ajax', 'core/notification'], function(ajax, notification) {\n return {\n /**\n * Get a list of tool types from Moodle for the given\n * search args.\n *\n * See also:\n * mod/lti/classes/external.php get_tool_types_parameters()\n *\n * @method query\n * @public\n * @param {Object} args Search parameters\n * @return {Promise} jQuery Deferred object\n */\n query: function(args) {\n var request = {\n methodname: 'mod_lti_get_tool_types',\n args: args || {}\n };\n\n var promise = ajax.call([request])[0];\n\n promise.fail(notification.exception);\n\n return promise;\n },\n\n /**\n * Create a tool type in Moodle.\n *\n * The promise will fail if the URL is not a cartridge, so you must handle the fail result.\n *\n * See also:\n * mod/lti/classes/external.php create_tool_type_parameters()\n *\n * @method create\n * @public\n * @param {Object} args Tool type properties\n * @return {Promise} jQuery Deferred object\n */\n create: function(args) {\n var request = {\n methodname: 'mod_lti_create_tool_type',\n args: args\n };\n\n var promise = ajax.call([request])[0];\n\n return promise;\n },\n\n /**\n * Update a tool type in Moodle.\n *\n * See also:\n * mod/lti/classes/external.php update_tool_type_parameters()\n *\n * @method update\n * @public\n * @param {Object} args Tool type properties\n * @return {Promise} jQuery Deferred object\n */\n update: function(args) {\n var request = {\n methodname: 'mod_lti_update_tool_type',\n args: args\n };\n\n var promise = ajax.call([request])[0];\n\n promise.fail(notification.exception);\n\n return promise;\n },\n\n /**\n * Delete a tool type from Moodle.\n *\n * @method delete\n * @public\n * @param {Integer} id Tool type ID\n * @return {Promise} jQuery Deferred object\n */\n 'delete': function(id) {\n var request = {\n methodname: 'mod_lti_delete_tool_type',\n args: {\n id: id\n }\n };\n\n var promise = ajax.call([request])[0];\n\n promise.fail(notification.exception);\n\n return promise;\n },\n\n /**\n * Get a list of tool types from Moodle for the given\n * tool proxy id.\n *\n * @method query\n * @public\n * @param {Integer} id Tool type ID\n * @return {Promise} jQuery Deferred object\n */\n getFromToolProxyId: function(id) {\n return this.query({toolproxyid: id});\n },\n\n /**\n * Check if the given URL is a cartridge URL.\n *\n * The promise will fail if the URL is unreachable, so you must handle the fail result.\n *\n * @method isCartridge\n * @public\n * @param {String} url\n * @return {Promise} jQuery Deferred object\n */\n isCartridge: function(url) {\n var request = {\n methodname: 'mod_lti_is_cartridge',\n args: {\n url: url\n }\n };\n\n var promise = ajax.call([request])[0];\n\n return promise;\n },\n\n /**\n * Tool type constants.\n */\n constants: {\n state: {\n configured: 1,\n pending: 2,\n rejected: 3\n },\n }\n };\n});\n"],"names":["define","ajax","notification","query","args","request","methodname","promise","call","fail","exception","create","update","id","getFromToolProxyId","this","toolproxyid","isCartridge","url","constants","state","configured","pending","rejected"],"mappings":";;;;;;;;AAuBAA,2BAAO,CAAC,YAAa,sBAAsB,SAASC,KAAMC,oBAC/C,CAaHC,MAAO,SAASC,UACRC,QAAU,CACVC,WAAY,yBACZF,KAAMA,MAAQ,IAGdG,QAAUN,KAAKO,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKP,aAAaQ,WAEnBH,SAgBXI,OAAQ,SAASP,UACTC,QAAU,CACVC,WAAY,2BACZF,KAAMA,aAGIH,KAAKO,KAAK,CAACH,UAAU,IAgBvCO,OAAQ,SAASR,UACTC,QAAU,CACVC,WAAY,2BACZF,KAAMA,MAGNG,QAAUN,KAAKO,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKP,aAAaQ,WAEnBH,gBAWD,SAASM,QACXR,QAAU,CACVC,WAAY,2BACZF,KAAM,CACFS,GAAIA,KAIRN,QAAUN,KAAKO,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKP,aAAaQ,WAEnBH,SAYXO,mBAAoB,SAASD,WAClBE,KAAKZ,MAAM,CAACa,YAAaH,MAapCI,YAAa,SAASC,SACdb,QAAU,CACVC,WAAY,uBACZF,KAAM,CACFc,IAAKA,aAICjB,KAAKO,KAAK,CAACH,UAAU,IAQvCc,UAAW,CACPC,MAAO,CACHC,WAAY,EACZC,QAAS,EACTC,SAAU"}amd/build/mod_form.min.js000064400000002506152265520610011335 0ustar00define("mod_lti/mod_form",["exports","mod_lti/contentitem"],(function(_exports,_contentitem){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_contentitem=(obj=_contentitem)&&obj.__esModule?obj:{default:obj};var _default={init:(courseId,toolUrl)=>{const contentItemButton=document.querySelector('[name="selectcontent"]');contentItemButton&&contentItemButton.addEventListener("click",(()=>{const contentItemUrl=contentItemButton.getAttribute("data-contentitemurl"),contentItemId=document.querySelector("#hidden_typeid").value;if(contentItemId){const title=document.querySelector("#id_name").value.trim(),text=document.querySelector("#id_introeditor").value.trim(),postData={id:contentItemId,course:courseId,title:title,text:text};_contentitem.default.init(contentItemUrl,toolUrl,postData,(returnData=>{if(!returnData.multiple){const allowGrades=document.querySelector("#id_instructorchoiceacceptgrades");let allowGradesChangeEvent=new Event("change");if(allowGrades.dispatchEvent(allowGradesChangeEvent),allowGrades.checked){const gradeType=document.querySelector("#id_grade_modgrade_type");gradeType.value="point";let gradeTypeChangeEvent=new Event("change");gradeType.dispatchEvent(gradeTypeChangeEvent)}}}))}}))}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=mod_form.min.js.mapamd/build/cartridge_registration_form.min.js000064400000005065152265520610015317 0ustar00/** * Encapsules the behavior for creating a tool type from a cartridge URL * in Moodle. Manages the UI while operations are occuring. * * See template: mod_lti/cartridge_registration_form * * @module mod_lti/cartridge_registration_form * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/cartridge_registration_form",["jquery","core/ajax","core/notification","mod_lti/tool_type","mod_lti/events","mod_lti/keys","core/str"],(function($,ajax,notification,toolType,ltiEvents,KEYS,str){var SELECTORS_CONSUMER_KEY="#registration-key",SELECTORS_SHARED_SECRET="#registration-secret",SELECTORS_REGISTRATION_FORM="#cartridge-registration-form",SELECTORS_REGISTRATION_SUBMIT_BUTTON="#cartridge-registration-submit",SELECTORS_REGISTRATION_CANCEL_BUTTON="#cartridge-registration-cancel",getSubmitButton=function(){return $(SELECTORS_REGISTRATION_SUBMIT_BUTTON)},submitCartridgeURL=function(){if(getSubmitButton().hasClass("loading"))return!1;var url=$(SELECTORS_REGISTRATION_FORM).attr("data-cartridge-url");if(""===url)return!1;getSubmitButton().addClass("loading");var consumerKey=$(SELECTORS_CONSUMER_KEY).val(),sharedSecret=$(SELECTORS_SHARED_SECRET).val(),promise=toolType.create({cartridgeurl:url,key:consumerKey,secret:sharedSecret});return promise.done((function(){str.get_string("successfullycreatedtooltype","mod_lti").done((function(s){$(document).trigger(ltiEvents.NEW_TOOL_TYPE),$(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION),$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,{message:s})})).fail(notification.exception)})).fail((function(){str.get_string("failedtocreatetooltype","mod_lti").done((function(s){$(document).trigger(ltiEvents.NEW_TOOL_TYPE),$(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION),$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,{message:s,error:!0})})).fail(notification.exception)})).always((function(){getSubmitButton().removeClass("loading")})),promise},registerEventListeners=function(){$(SELECTORS_REGISTRATION_FORM).submit((function(e){e.preventDefault(),submitCartridgeURL()}));var cancelButton=$(SELECTORS_REGISTRATION_CANCEL_BUTTON);cancelButton.click((function(e){e.preventDefault(),$(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION)})),cancelButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),cancelButton.click())}))};return{init:function(){registerEventListeners()}}})); //# sourceMappingURL=cartridge_registration_form.min.js.mapamd/build/external_registration_return.min.js.map000064400000005007152265520610016321 0ustar00{"version":3,"file":"external_registration_return.min.js","sources":["../src/external_registration_return.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handles the return params from the external registration page after it\n * redirects back to Moodle.\n *\n * See also: mod/lti/externalregistrationreturn.php\n *\n * @module mod_lti/external_registration_return\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine([], function() {\n return {\n\n /**\n * If this was rendered in an iframe then trigger the external registration\n * complete behaviour in the parent page and provide the params returned from\n * the external registration page.\n *\n * @param {String} message The registration message from the external registration page\n * @param {String} error The registration error message from the external registration page, if\n * there was an error.\n * @param {Integer} id The tool proxy id for the external registration.\n * @param {String} status Whether the external registration was successful or not.\n */\n init: function(message, error, id, status) {\n if (window.parent) {\n window.parent.triggerExternalRegistrationComplete({\n message: message,\n error: error,\n id: id,\n status: status\n });\n }\n }\n };\n});\n"],"names":["define","init","message","error","id","status","window","parent","triggerExternalRegistrationComplete"],"mappings":";;;;;;;;;;;AA0BAA,8CAAO,IAAI,iBACA,CAaHC,KAAM,SAASC,QAASC,MAAOC,GAAIC,QAC3BC,OAAOC,QACPD,OAAOC,OAAOC,oCAAoC,CAC9CN,QAASA,QACTC,MAAOA,MACPC,GAAIA,GACJC,OAAQA"}amd/build/cartridge_registration_form.min.js.map000064400000021226152265520610016070 0ustar00{"version":3,"file":"cartridge_registration_form.min.js","sources":["../src/cartridge_registration_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Encapsules the behavior for creating a tool type from a cartridge URL\n * in Moodle. Manages the UI while operations are occuring.\n *\n * See template: mod_lti/cartridge_registration_form\n *\n * @module mod_lti/cartridge_registration_form\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/notification', 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys', 'core/str'],\n function($, ajax, notification, toolType, ltiEvents, KEYS, str) {\n\n var SELECTORS = {\n CARTRIDGE_URL: '#cartridge-url',\n CONSUMER_KEY: '#registration-key',\n SHARED_SECRET: '#registration-secret',\n REGISTRATION_FORM: '#cartridge-registration-form',\n REGISTRATION_SUBMIT_BUTTON: '#cartridge-registration-submit',\n REGISTRATION_CANCEL_BUTTON: '#cartridge-registration-cancel',\n };\n\n /**\n * Return the URL the user entered for the cartridge.\n *\n * @method getCartridgeURL\n * @private\n * @return {String}\n */\n var getCartridgeURL = function() {\n return $(SELECTORS.REGISTRATION_FORM).attr('data-cartridge-url');\n };\n\n /**\n * Return the submit button element.\n *\n * @method getSubmitButton\n * @private\n * @return {JQuery} jQuery object\n */\n var getSubmitButton = function() {\n return $(SELECTORS.REGISTRATION_SUBMIT_BUTTON);\n };\n\n /**\n * Return the cancel button element.\n *\n * @method getCancelButton\n * @private\n * @return {JQuery} jQuery object\n */\n var getCancelButton = function() {\n return $(SELECTORS.REGISTRATION_CANCEL_BUTTON);\n };\n\n /**\n * Return the value that the user entered for the consumer key.\n *\n * @method getConsumerKey\n * @private\n * @return {String} the value entered for consumer key.\n */\n var getConsumerKey = function() {\n return $(SELECTORS.CONSUMER_KEY).val();\n };\n\n /**\n * Return the value that the user entered for the shared secret.\n *\n * @method getSharedSecret\n * @private\n * @return {String} the value entered for shared secret\n */\n var getSharedSecret = function() {\n return $(SELECTORS.SHARED_SECRET).val();\n };\n\n /**\n * Trigger a visual loading indicator.\n *\n * @method startLoading\n * @private\n */\n var startLoading = function() {\n getSubmitButton().addClass('loading');\n };\n\n /**\n * Stop the visual loading indicator.\n *\n * @method stopLoading\n * @private\n */\n var stopLoading = function() {\n getSubmitButton().removeClass('loading');\n };\n\n /**\n * Check if the page is currently loading.\n *\n * @method isLoading\n * @private\n * @return {Boolean}\n */\n var isLoading = function() {\n return getSubmitButton().hasClass('loading');\n };\n\n /**\n * Create a tool type from the cartridge URL that the user input. This will\n * send an ajax request to the Moodle server to create the Type. The request will\n * include the consumer key and secret, if any.\n *\n * On success the page will be re-rendered to take the user back to the original\n * page with the list of tools and an alert notifying them of success.\n *\n * @method submitCartridgeURL\n * @private\n * @return {Promise} jQuery Deferred object\n */\n var submitCartridgeURL = function() {\n if (isLoading()) {\n return false;\n }\n\n var url = getCartridgeURL();\n // No URL? Do nothing.\n if (url === \"\") {\n return false;\n }\n\n startLoading();\n var consumerKey = getConsumerKey();\n var sharedSecret = getSharedSecret();\n var promise = toolType.create({cartridgeurl: url, key: consumerKey, secret: sharedSecret});\n\n promise.done(function() {\n str.get_string('successfullycreatedtooltype', 'mod_lti').done(function(s) {\n $(document).trigger(ltiEvents.NEW_TOOL_TYPE);\n $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION);\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, {\n message: s\n });\n }).fail(notification.exception);\n }).fail(function() {\n str.get_string('failedtocreatetooltype', 'mod_lti').done(function(s) {\n $(document).trigger(ltiEvents.NEW_TOOL_TYPE);\n $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION);\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, {\n message: s,\n error: true\n });\n }).fail(notification.exception);\n }).always(function() {\n stopLoading();\n });\n\n return promise;\n };\n\n /**\n * Sets up the listeners for user interaction on the page.\n *\n * @method registerEventListeners\n * @private\n */\n var registerEventListeners = function() {\n var form = $(SELECTORS.REGISTRATION_FORM);\n form.submit(function(e) {\n e.preventDefault();\n submitCartridgeURL();\n });\n\n var cancelButton = getCancelButton();\n cancelButton.click(function(e) {\n e.preventDefault();\n $(document).trigger(ltiEvents.STOP_CARTRIDGE_REGISTRATION);\n });\n cancelButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n cancelButton.click();\n }\n }\n });\n };\n\n return /** @alias module:mod_lti/cartridge_registration_form */ {\n\n /**\n * Initialise this module.\n */\n init: function() {\n registerEventListeners();\n }\n };\n});\n"],"names":["define","$","ajax","notification","toolType","ltiEvents","KEYS","str","SELECTORS","getSubmitButton","submitCartridgeURL","hasClass","url","attr","addClass","consumerKey","val","sharedSecret","promise","create","cartridgeurl","key","secret","done","get_string","s","document","trigger","NEW_TOOL_TYPE","STOP_CARTRIDGE_REGISTRATION","REGISTRATION_FEEDBACK","message","fail","exception","error","always","removeClass","registerEventListeners","submit","e","preventDefault","cancelButton","click","keypress","metaKey","shiftKey","altKey","ctrlKey","keyCode","ENTER","SPACE","init"],"mappings":";;;;;;;;;;;AA0BAA,6CAAO,CAAC,SAAU,YAAa,oBAAqB,oBAAqB,iBAAkB,eAAgB,aACnG,SAASC,EAAGC,KAAMC,aAAcC,SAAUC,UAAWC,KAAMC,SAE3DC,uBAEc,oBAFdA,wBAGe,uBAHfA,4BAImB,+BAJnBA,qCAK4B,iCAL5BA,qCAM4B,iCAqB5BC,gBAAkB,kBACXR,EAAEO,uCA+ETE,mBAAqB,cAfdD,kBAAkBE,SAAS,kBAiBvB,MAGPC,IA/FGX,EAAEO,6BAA6BK,KAAK,yBAiG/B,KAARD,WACO,EA5CXH,kBAAkBK,SAAS,eAgDvBC,YArEGd,EAAEO,wBAAwBQ,MAsE7BC,aA3DGhB,EAAEO,yBAAyBQ,MA4D9BE,QAAUd,SAASe,OAAO,CAACC,aAAcR,IAAKS,IAAKN,YAAaO,OAAQL,sBAE5EC,QAAQK,MAAK,WACThB,IAAIiB,WAAW,8BAA+B,WAAWD,MAAK,SAASE,GACnExB,EAAEyB,UAAUC,QAAQtB,UAAUuB,eAC9B3B,EAAEyB,UAAUC,QAAQtB,UAAUwB,6BAC9B5B,EAAEyB,UAAUC,QAAQtB,UAAUyB,sBAAuB,CACjDC,QAASN,OAEdO,KAAK7B,aAAa8B,cACtBD,MAAK,WACJzB,IAAIiB,WAAW,yBAA0B,WAAWD,MAAK,SAASE,GAC9DxB,EAAEyB,UAAUC,QAAQtB,UAAUuB,eAC9B3B,EAAEyB,UAAUC,QAAQtB,UAAUwB,6BAC9B5B,EAAEyB,UAAUC,QAAQtB,UAAUyB,sBAAuB,CACjDC,QAASN,EACTS,OAAO,OAEZF,KAAK7B,aAAa8B,cACtBE,QAAO,WA3DV1B,kBAAkB2B,YAAY,cA+DvBlB,SASPmB,uBAAyB,WACdpC,EAAEO,6BACR8B,QAAO,SAASC,GACjBA,EAAEC,iBACF9B,4BAGA+B,aAzHGxC,EAAEO,sCA0HTiC,aAAaC,OAAM,SAASH,GACxBA,EAAEC,iBACFvC,EAAEyB,UAAUC,QAAQtB,UAAUwB,gCAElCY,aAAaE,UAAS,SAASJ,GACtBA,EAAEK,SAAYL,EAAEM,UAAaN,EAAEO,QAAWP,EAAEQ,SACzCR,EAAES,SAAW1C,KAAK2C,OAASV,EAAES,SAAW1C,KAAK4C,QAC7CX,EAAEC,iBACFC,aAAaC,mBAMmC,CAK5DS,KAAM,WACFd"}amd/build/tool_configure_controller.min.js.map000064400000053654152265520610015602 0ustar00{"version":3,"file":"tool_configure_controller.min.js","sources":["../src/tool_configure_controller.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Standard Ajax wrapper for Moodle. It calls the central Ajax script,\n * which can call any existing webservice using the current session.\n * In addition, it can batch multiple requests and return multiple responses.\n *\n * @module mod_lti/tool_configure_controller\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/paged_content_factory', 'core/notification', 'core/templates', 'mod_lti/events',\n 'mod_lti/keys', 'mod_lti/tool_types_and_proxies', 'mod_lti/tool_type', 'mod_lti/tool_proxy', 'core/str', 'core/config'],\n function($, ajax,\n pagedContentFactory, notification, templates, ltiEvents, KEYS,\n toolTypesAndProxies, toolType, toolProxy, str, config) {\n\n var SELECTORS = {\n EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-container',\n EXTERNAL_REGISTRATION_PAGE_CONTAINER: '#external-registration-page-container',\n EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER: '#external-registration-template-container',\n CARTRIDGE_REGISTRATION_CONTAINER: '#cartridge-registration-container',\n CARTRIDGE_REGISTRATION_FORM: '#cartridge-registration-form',\n ADD_TOOL_FORM: '#add-tool-form',\n TOOL_CARD_CONTAINER: '#tool-card-container',\n TOOL_LIST_CONTAINER: '#tool-list-container',\n TOOL_CREATE_BUTTON: '#tool-create-button',\n TOOL_CREATE_LTILEGACY_BUTTON: '#tool-createltilegacy-button',\n REGISTRATION_CHOICE_CONTAINER: '#registration-choice-container',\n TOOL_URL: '#tool-url'\n };\n\n /**\n * Get the tool list container element.\n *\n * @method getToolListContainer\n * @private\n * @return {Object} jQuery object\n */\n var getToolListContainer = function() {\n return $(SELECTORS.TOOL_LIST_CONTAINER);\n };\n\n /**\n * Get the tool card container element.\n *\n * @method getToolCardContainer\n * @private\n * @return {Object} jQuery object\n */\n const getToolCardContainer = function() {\n return $(SELECTORS.TOOL_CARD_CONTAINER);\n };\n\n /**\n * Get the external registration container element.\n *\n * @method getExternalRegistrationContainer\n * @private\n * @return {Object} jQuery object\n */\n var getExternalRegistrationContainer = function() {\n return $(SELECTORS.EXTERNAL_REGISTRATION_CONTAINER);\n };\n\n /**\n * Get the cartridge registration container element.\n *\n * @method getCartridgeRegistrationContainer\n * @private\n * @return {Object} jQuery object\n */\n var getCartridgeRegistrationContainer = function() {\n return $(SELECTORS.CARTRIDGE_REGISTRATION_CONTAINER);\n };\n\n /**\n * Get the registration choice container element.\n *\n * @method getRegistrationChoiceContainer\n * @private\n * @return {Object} jQuery object\n */\n var getRegistrationChoiceContainer = function() {\n return $(SELECTORS.REGISTRATION_CHOICE_CONTAINER);\n };\n\n /**\n * Close the LTI Advantage Registration IFrame.\n *\n * @private\n * @param {Object} e post message event sent from the registration frame.\n */\n var closeLTIAdvRegistration = function(e) {\n if (e.data && 'org.imsglobal.lti.close' === e.data.subject) {\n $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty();\n hideExternalRegistration();\n showRegistrationChoices();\n showToolList();\n showRegistrationChoices();\n reloadToolList();\n }\n };\n\n /**\n * Load the external registration template and render it in the DOM and display it.\n *\n * @method initiateRegistration\n * @private\n * @param {String} url where to send the registration request\n */\n var initiateRegistration = function(url) {\n // Show the external registration page in an iframe.\n $(SELECTORS.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass('hidden');\n var container = $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);\n container.append($(\"\"));\n showExternalRegistration();\n window.addEventListener(\"message\", closeLTIAdvRegistration, false);\n };\n\n /**\n * Get the tool type URL.\n *\n * @method getToolURL\n * @private\n * @return {String} the tool type url\n */\n var getToolURL = function() {\n return $(SELECTORS.TOOL_URL).val();\n };\n\n /**\n * Hide the external registration container.\n *\n * @method hideExternalRegistration\n * @private\n */\n var hideExternalRegistration = function() {\n getExternalRegistrationContainer().addClass('hidden');\n };\n\n /**\n * Hide the cartridge registration container.\n *\n * @method hideCartridgeRegistration\n * @private\n */\n var hideCartridgeRegistration = function() {\n getCartridgeRegistrationContainer().addClass('hidden');\n };\n\n /**\n * Hide the registration choice container.\n *\n * @method hideRegistrationChoices\n * @private\n */\n var hideRegistrationChoices = function() {\n getRegistrationChoiceContainer().addClass('hidden');\n };\n\n /**\n * Display the external registration panel and hides the other\n * panels.\n *\n * @method showExternalRegistration\n * @private\n */\n var showExternalRegistration = function() {\n hideCartridgeRegistration();\n hideRegistrationChoices();\n getExternalRegistrationContainer().removeClass('hidden');\n screenReaderAnnounce(getExternalRegistrationContainer());\n };\n\n /**\n * Display the cartridge registration panel and hides the other\n * panels.\n *\n * @method showCartridgeRegistration\n * @param {String} url\n * @private\n */\n var showCartridgeRegistration = function(url) {\n hideExternalRegistration();\n hideRegistrationChoices();\n // Don't save the key and secret from the last tool.\n var container = getCartridgeRegistrationContainer();\n container.find('input').val('');\n container.removeClass('hidden');\n container.find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).attr('data-cartridge-url', url);\n screenReaderAnnounce(container);\n };\n\n /**\n * Display the registration choices panel and hides the other\n * panels.\n *\n * @method showRegistrationChoices\n * @private\n */\n var showRegistrationChoices = function() {\n hideExternalRegistration();\n hideCartridgeRegistration();\n getRegistrationChoiceContainer().removeClass('hidden');\n screenReaderAnnounce(getRegistrationChoiceContainer());\n };\n\n /**\n * JAWS does not notice visibility changes with aria-live.\n * Remove and add the content back to force it to read it out.\n * This function can be removed once JAWS supports visibility.\n *\n * @method screenReaderAnnounce\n * @param {Object} element\n * @private\n */\n var screenReaderAnnounce = function(element) {\n var children = element.children().detach();\n children.appendTo(element);\n };\n\n /**\n * Hides the list of tool types.\n *\n * @method hideToolList\n * @private\n */\n var hideToolList = function() {\n getToolListContainer().addClass('hidden');\n };\n\n /**\n * Display the list of tool types.\n *\n * @method hideToolList\n * @private\n */\n var showToolList = function() {\n getToolListContainer().removeClass('hidden');\n };\n\n /**\n * Display the registration feedback alert and hide the other panels.\n *\n * @method showRegistrationFeedback\n * @param {Object} data\n * @private\n */\n var showRegistrationFeedback = function(data) {\n var type = data.error ? 'error' : 'success';\n notification.addNotification({\n message: data.message,\n type: type\n });\n };\n\n /**\n * Show the loading animation\n *\n * @method startLoading\n * @private\n * @param {Object} element jQuery object\n */\n var startLoading = function(element) {\n element.addClass(\"loading\");\n };\n\n /**\n * Hide the loading animation\n *\n * @method stopLoading\n * @private\n * @param {Object} element jQuery object\n */\n var stopLoading = function(element) {\n element.removeClass(\"loading\");\n };\n\n /**\n * Refresh the list of tool types and render the new ones.\n *\n * @method reloadToolList\n * @private\n */\n var reloadToolList = function() {\n // Behat tests should wait for the tool list to load.\n M.util.js_pending('reloadToolList');\n\n const cardContainer = getToolCardContainer();\n const listContainer = getToolListContainer();\n const limit = 60;\n // Get initial data with zero limit and offset.\n fetchToolCount().done(function(data) {\n pagedContentFactory.createWithTotalAndLimit(\n data.count,\n limit,\n function(pagesData) {\n return pagesData.map(function(pageData) {\n return fetchToolData(pageData.limit, pageData.offset)\n .then(function(data) {\n return renderToolData(data);\n });\n });\n },\n {\n 'showFirstLast': true\n })\n .done(function(html, js) {\n // Add the paged content into the page.\n templates.replaceNodeContents(cardContainer, html, js);\n })\n .always(function() {\n stopLoading(listContainer);\n M.util.js_complete('reloadToolList');\n });\n });\n startLoading(listContainer);\n };\n\n /**\n * Fetch the count of tool type and proxy datasets.\n *\n * @return {*|void}\n */\n const fetchToolCount = function() {\n return toolTypesAndProxies.count({'orphanedonly': true})\n .done(function(data) {\n return data;\n }).catch(function(error) {\n // Add debug message, then return empty data.\n notification.exception(error);\n return {\n 'count': 0\n };\n });\n };\n\n /**\n * Fetch the data for tool type and proxy cards.\n *\n * @param {number} limit Maximum number of datasets to get.\n * @param {number} offset Offset count for fetching the data.\n * @return {*|void}\n */\n const fetchToolData = function(limit, offset) {\n const args = {'orphanedonly': true};\n // Only add limit and offset to args if they are integers and not null, otherwise defaults will be used.\n if (limit !== null && !Number.isNaN(limit)) {\n args.limit = limit;\n }\n if (offset !== null && !Number.isNaN(offset)) {\n args.offset = offset;\n }\n return toolTypesAndProxies.query(args)\n .done(function(data) {\n return data;\n }).catch(function(error) {\n // Add debug message, then return empty data.\n notification.exception(error);\n return {\n 'types': [],\n 'proxies': [],\n 'limit': limit,\n 'offset': offset\n };\n });\n };\n\n /**\n * Render Tool and Proxy cards from data.\n *\n * @param {Object} data Contains arrays of data objects to populate cards.\n * @return {*}\n */\n const renderToolData = function(data) {\n const context = {\n tools: data.types,\n proxies: data.proxies,\n };\n return templates.render('mod_lti/tool_list', context)\n .done(function(html, js) {\n return {html, js};\n }\n );\n };\n\n /**\n * Start the LTI Advantage registration.\n *\n * @method addLTIAdvTool\n * @private\n */\n var addLTIAdvTool = function() {\n var url = getToolURL().trim();\n\n if (url) {\n $(SELECTORS.TOOL_URL).val('');\n hideToolList();\n initiateRegistration(url);\n }\n\n };\n\n /**\n * Trigger appropriate registration process process for the user input\n * URL. It can either be a cartridge or a registration url.\n *\n * @method addLTILegacyTool\n * @private\n * @return {Promise} jQuery Deferred object\n */\n var addLTILegacyTool = function() {\n var url = getToolURL().trim();\n\n if (url === \"\") {\n return $.Deferred().resolve();\n }\n var toolButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON);\n startLoading(toolButton);\n\n var promise = toolType.isCartridge(url);\n\n promise.always(function() {\n stopLoading(toolButton);\n });\n\n promise.done(function(result) {\n if (result.iscartridge) {\n $(SELECTORS.TOOL_URL).val('');\n $(document).trigger(ltiEvents.START_CARTRIDGE_REGISTRATION, url);\n } else {\n $(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION, {url: url});\n }\n });\n\n promise.fail(function() {\n str.get_string('errorbadurl', 'mod_lti')\n .done(function(s) {\n $(document).trigger(ltiEvents.REGISTRATION_FEEDBACK, {\n message: s,\n error: true\n });\n })\n .fail(notification.exception);\n });\n\n return promise;\n };\n\n /**\n * Sets up the listeners for user interaction on the page.\n *\n * @method registerEventListeners\n * @private\n */\n var registerEventListeners = function() {\n\n // These are events fired by the registration processes. Either\n // the cartridge registration or the external registration url.\n $(document).on(ltiEvents.NEW_TOOL_TYPE, function() {\n reloadToolList();\n });\n\n $(document).on(ltiEvents.START_EXTERNAL_REGISTRATION, function() {\n showExternalRegistration();\n $(SELECTORS.TOOL_URL).val('');\n hideToolList();\n });\n\n $(document).on(ltiEvents.STOP_EXTERNAL_REGISTRATION, function() {\n showToolList();\n showRegistrationChoices();\n });\n\n $(document).on(ltiEvents.START_CARTRIDGE_REGISTRATION, function(event, url) {\n showCartridgeRegistration(url);\n });\n\n $(document).on(ltiEvents.STOP_CARTRIDGE_REGISTRATION, function() {\n getCartridgeRegistrationContainer().find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).removeAttr('data-cartridge-url');\n showRegistrationChoices();\n });\n\n $(document).on(ltiEvents.REGISTRATION_FEEDBACK, function(event, data) {\n showRegistrationFeedback(data);\n });\n\n var addLegacyButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON);\n addLegacyButton.click(function(e) {\n e.preventDefault();\n addLTILegacyTool();\n });\n\n var addLTIButton = $(SELECTORS.TOOL_CREATE_BUTTON);\n addLTIButton.click(function(e) {\n e.preventDefault();\n addLTIAdvTool();\n });\n\n };\n\n return /** @alias module:mod_lti/cartridge_registration_form */ {\n\n /**\n * Initialise this module.\n */\n init: function() {\n registerEventListeners();\n reloadToolList();\n }\n };\n});\n"],"names":["define","$","ajax","pagedContentFactory","notification","templates","ltiEvents","KEYS","toolTypesAndProxies","toolType","toolProxy","str","config","SELECTORS","getToolListContainer","getExternalRegistrationContainer","getCartridgeRegistrationContainer","getRegistrationChoiceContainer","closeLTIAdvRegistration","e","data","subject","empty","hideExternalRegistration","showRegistrationChoices","showToolList","reloadToolList","getToolURL","val","addClass","hideCartridgeRegistration","hideRegistrationChoices","showExternalRegistration","removeClass","screenReaderAnnounce","element","children","detach","appendTo","hideToolList","startLoading","stopLoading","M","util","js_pending","cardContainer","listContainer","fetchToolCount","done","createWithTotalAndLimit","count","pagesData","map","pageData","fetchToolData","limit","offset","then","renderToolData","html","js","replaceNodeContents","always","js_complete","catch","error","exception","args","Number","isNaN","query","context","tools","types","proxies","render","addLTIAdvTool","url","trim","append","encodeURIComponent","sesskey","window","addEventListener","initiateRegistration","registerEventListeners","document","on","NEW_TOOL_TYPE","START_EXTERNAL_REGISTRATION","STOP_EXTERNAL_REGISTRATION","START_CARTRIDGE_REGISTRATION","event","container","find","attr","showCartridgeRegistration","STOP_CARTRIDGE_REGISTRATION","removeAttr","REGISTRATION_FEEDBACK","type","addNotification","message","showRegistrationFeedback","click","preventDefault","Deferred","resolve","toolButton","promise","isCartridge","result","iscartridge","trigger","fail","get_string","s","addLTILegacyTool","init"],"mappings":";;;;;;;;;;AAyBAA,2CAAO,CAAC,SAAU,YAAa,6BAA8B,oBAAqB,iBAAkB,iBAC5F,eAAgB,iCAAkC,oBAAqB,qBAAsB,WAAY,gBACzG,SAASC,EAAGC,KACHC,oBAAqBC,aAAcC,UAAWC,UAAWC,KACzDC,oBAAqBC,SAAUC,UAAWC,IAAKC,YAExDC,0CACiC,mCADjCA,+CAEsC,wCAFtCA,mDAG0C,4CAH1CA,2CAIkC,oCAJlCA,sCAK6B,+BAL7BA,8BAOqB,uBAPrBA,8BAQqB,uBARrBA,6BASoB,sBATpBA,uCAU8B,+BAV9BA,wCAW+B,iCAX/BA,mBAYU,YAUVC,qBAAuB,kBAChBb,EAAEY,oCAqBTE,iCAAmC,kBAC5Bd,EAAEY,4CAUTG,kCAAoC,kBAC7Bf,EAAEY,6CAUTI,+BAAiC,kBAC1BhB,EAAEY,0CASTK,wBAA0B,SAASC,GAC/BA,EAAEC,MAAQ,4BAA8BD,EAAEC,KAAKC,UAC/CpB,EAAEY,oDAAoDS,QACtDC,2BACAC,0BACAC,eACAD,0BACAE,mBA4BJC,WAAa,kBACN1B,EAAEY,oBAAoBe,OAS7BL,yBAA2B,WAC3BR,mCAAmCc,SAAS,WAS5CC,0BAA4B,WAC5Bd,oCAAoCa,SAAS,WAS7CE,wBAA0B,WAC1Bd,iCAAiCY,SAAS,WAU1CG,yBAA2B,WAC3BF,4BACAC,0BACAhB,mCAAmCkB,YAAY,UAC/CC,qBAAqBnB,qCA6BrBS,wBAA0B,WAC1BD,2BACAO,4BACAb,iCAAiCgB,YAAY,UAC7CC,qBAAqBjB,mCAYrBiB,qBAAuB,SAASC,SACjBA,QAAQC,WAAWC,SACzBC,SAASH,UASlBI,aAAe,WACfzB,uBAAuBe,SAAS,WAShCJ,aAAe,WACfX,uBAAuBmB,YAAY,WAyBnCO,aAAe,SAASL,SACxBA,QAAQN,SAAS,YAUjBY,YAAc,SAASN,SACvBA,QAAQF,YAAY,YASpBP,eAAiB,WAEjBgB,EAAEC,KAAKC,WAAW,wBAEZC,cA/OC5C,EAAEY,+BAgPHiC,cAAgBhC,uBAGtBiC,iBAAiBC,MAAK,SAAS5B,MAC3BjB,oBAAoB8C,wBAChB7B,KAAK8B,MAJC,IAMN,SAASC,kBACEA,UAAUC,KAAI,SAASC,iBACnBC,cAAcD,SAASE,MAAOF,SAASG,QACzCC,MAAK,SAASrC,aACJsC,eAAetC,cAItC,gBACqB,IAEpB4B,MAAK,SAASW,KAAMC,IAErBvD,UAAUwD,oBAAoBhB,cAAec,KAAMC,OAElDE,QAAO,WACJrB,YAAYK,eACZJ,EAAEC,KAAKoB,YAAY,wBAG/BvB,aAAaM,sBAQXC,eAAiB,kBACZvC,oBAAoB0C,MAAM,eAAiB,IAC7CF,MAAK,SAAS5B,aACJA,QACR4C,OAAM,SAASC,cAEd7D,aAAa8D,UAAUD,OAChB,OACM,OAYnBX,cAAgB,SAASC,MAAOC,cAC5BW,KAAO,eAAiB,UAEhB,OAAVZ,OAAmBa,OAAOC,MAAMd,SAChCY,KAAKZ,MAAQA,OAEF,OAAXC,QAAoBY,OAAOC,MAAMb,UACjCW,KAAKX,OAASA,QAEXhD,oBAAoB8D,MAAMH,MAC5BnB,MAAK,SAAS5B,aACJA,QACR4C,OAAM,SAASC,cAEd7D,aAAa8D,UAAUD,OAChB,OACM,WACE,SACFV,aACCC,YAWpBE,eAAiB,SAAStC,YACtBmD,QAAU,CACZC,MAAOpD,KAAKqD,MACZC,QAAStD,KAAKsD,gBAEXrE,UAAUsE,OAAO,oBAAqBJ,SACxCvB,MAAK,SAASW,KAAMC,UACN,CAACD,KAAAA,KAAMC,GAAAA,YAW1BgB,cAAgB,eACZC,IAAMlD,aAAamD,OAEnBD,MACA5E,EAAEY,oBAAoBe,IAAI,IAC1BW,eAhSmB,SAASsC,KAEhC5E,EAAEY,gDAAgDoB,YAAY,UAC9ChC,EAAEY,oDACRkE,OAAO9E,EAAE,gDACA+E,mBAAmBH,KAAO,YAAcjE,OAAOqE,QAAU,gBAC5EjD,2BACAkD,OAAOC,iBAAiB,UAAWjE,yBAAyB,GA0RxDkE,CAAqBP,OAyDzBQ,uBAAyB,WAIzBpF,EAAEqF,UAAUC,GAAGjF,UAAUkF,eAAe,WACpC9D,oBAGJzB,EAAEqF,UAAUC,GAAGjF,UAAUmF,6BAA6B,WAClDzD,2BACA/B,EAAEY,oBAAoBe,IAAI,IAC1BW,kBAGJtC,EAAEqF,UAAUC,GAAGjF,UAAUoF,4BAA4B,WACjDjE,eACAD,6BAGJvB,EAAEqF,UAAUC,GAAGjF,UAAUqF,8BAA8B,SAASC,MAAOf,MApS3C,SAASA,KACrCtD,2BACAQ,8BAEI8D,UAAY7E,oCAChB6E,UAAUC,KAAK,SAASlE,IAAI,IAC5BiE,UAAU5D,YAAY,UACtB4D,UAAUC,KAAKjF,uCAAuCkF,KAAK,qBAAsBlB,KACjF3C,qBAAqB2D,WA6RjBG,CAA0BnB,QAG9B5E,EAAEqF,UAAUC,GAAGjF,UAAU2F,6BAA6B,WAClDjF,oCAAoC8E,KAAKjF,uCAAuCqF,WAAW,sBAC3F1E,6BAGJvB,EAAEqF,UAAUC,GAAGjF,UAAU6F,uBAAuB,SAASP,MAAOxE,OA3OrC,SAASA,UAChCgF,KAAOhF,KAAK6C,MAAQ,QAAU,UAClC7D,aAAaiG,gBAAgB,CACzBC,QAASlF,KAAKkF,QACdF,KAAMA,OAwONG,CAAyBnF,SAGPnB,EAAEY,wCACR2F,OAAM,SAASrF,GAC3BA,EAAEsF,iBA9Ea,eACf5B,IAAMlD,aAAamD,UAEX,KAARD,WACO5E,EAAEyG,WAAWC,cAEpBC,WAAa3G,EAAEY,wCACnB2B,aAAaoE,gBAETC,QAAUpG,SAASqG,YAAYjC,KAEnCgC,QAAQ/C,QAAO,WACbrB,YAAYmE,eAGdC,QAAQ7D,MAAK,SAAS+D,QACdA,OAAOC,aACP/G,EAAEY,oBAAoBe,IAAI,IAC1B3B,EAAEqF,UAAU2B,QAAQ3G,UAAUqF,6BAA8Bd,MAE5D5E,EAAEqF,UAAU2B,QAAQ3G,UAAUmF,4BAA6B,CAACZ,IAAKA,SAIzEgC,QAAQK,MAAK,WACTvG,IAAIwG,WAAW,cAAe,WACzBnE,MAAK,SAASoE,GACPnH,EAAEqF,UAAU2B,QAAQ3G,UAAU6F,sBAAuB,CAC7CG,QAASc,EACTnD,OAAO,OAGtBiD,KAAK9G,aAAa8D,cA+CvBmD,MAGepH,EAAEY,8BACR2F,OAAM,SAASrF,GACxBA,EAAEsF,iBACF7B,0BAKwD,CAK5D0C,KAAM,WACFjC,yBACA3D"}amd/build/tool_proxy.min.js000064400000001530152265520610011745 0ustar00/** * Provides an interface for a tool proxy in the Moodle server. * * @module mod_lti/tool_proxy * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/tool_proxy",["core/ajax","core/notification"],(function(ajax,notification){return{query:function(args){var request={methodname:"mod_lti_get_tool_proxies",args:args||{}},promise=ajax.call([request])[0];return promise.fail(notification.exception),promise},delete:function(id){var request={methodname:"mod_lti_delete_tool_proxy",args:{id:id}},promise=ajax.call([request])[0];return promise.fail(notification.exception),promise},create:function(args){var request={methodname:"mod_lti_create_tool_proxy",args:args};return ajax.call([request])[0]}}})); //# sourceMappingURL=tool_proxy.min.js.mapamd/build/tool_types_and_proxies.min.js000064400000001633152265520610014327 0ustar00define("mod_lti/tool_types_and_proxies",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * Provides an interface for external tools in the Moodle server. * * @module mod_lti/tool_types_and_proxies * @class tool_types_and_proxies * @copyright 2020 Andrew Madden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.query=_exports.count=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.query=args=>{const request={methodname:"mod_lti_get_tool_types_and_proxies",args:args||{}};return _ajax.default.call([request])[0]};_exports.count=args=>{const request={methodname:"mod_lti_get_tool_types_and_proxies_count",args:args||{}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=tool_types_and_proxies.min.js.mapamd/build/repository.min.js.map000064400000003377152265520610012535 0ustar00{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to handle AJAX interactions.\n *\n * @module mod_lti/repository\n * @copyright 2023 Ilya Tregubov \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Toggle coursevisible of a tool\n *\n * @param {Number} tooltypeid Too type id\n * @param {Number} courseid Course ID\n * @param {Number} showinactivitychooser showinactivitychooser state\n * @return {Promise}\n */\nexport const toggleShowInActivityChooser = (\n tooltypeid,\n courseid,\n showinactivitychooser,\n) => Ajax.call([{\n methodname: 'mod_lti_toggle_showinactivitychooser',\n args: {\n tooltypeid,\n courseid,\n showinactivitychooser,\n },\n}])[0];\n"],"names":["tooltypeid","courseid","showinactivitychooser","Ajax","call","methodname","args"],"mappings":";;;;;;;kMAiC2C,CACvCA,WACAC,SACAC,wBACCC,cAAKC,KAAK,CAAC,CACZC,WAAY,uCACZC,KAAM,CACFN,WAAAA,WACAC,SAAAA,SACAC,sBAAAA,0BAEJ"}amd/build/external_registration_return.min.js000064400000001214152265520610015541 0ustar00/** * Handles the return params from the external registration page after it * redirects back to Moodle. * * See also: mod/lti/externalregistrationreturn.php * * @module mod_lti/external_registration_return * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/external_registration_return",[],(function(){return{init:function(message,error,id,status){window.parent&&window.parent.triggerExternalRegistrationComplete({message:message,error:error,id:id,status:status})}}})); //# sourceMappingURL=external_registration_return.min.js.mapamd/build/events.min.js.map000064400000004053152265520610011612 0ustar00{"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Provides a list of events that can be triggered in the LTI management\n * page.\n *\n * @module mod_lti/events\n * @class events\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine([], function() {\n return /** @alias module:mod_lti/events */ {\n NEW_TOOL_TYPE: 'lti.tool.type.new',\n START_EXTERNAL_REGISTRATION: 'lti.registration.external.start',\n STOP_EXTERNAL_REGISTRATION: 'lti.registration.external.stop',\n START_CARTRIDGE_REGISTRATION: 'lti.registration.cartridge.start',\n STOP_CARTRIDGE_REGISTRATION: 'lti.registration.cartridge.stop',\n REGISTRATION_FEEDBACK: 'lti.registration.feedback',\n CAPABILITIES_AGREE: 'lti.tool.type.capabilities.agree',\n CAPABILITIES_DECLINE: 'lti.tool.type.capabilities.decline',\n };\n});\n"],"names":["define","NEW_TOOL_TYPE","START_EXTERNAL_REGISTRATION","STOP_EXTERNAL_REGISTRATION","START_CARTRIDGE_REGISTRATION","STOP_CARTRIDGE_REGISTRATION","REGISTRATION_FEEDBACK","CAPABILITIES_AGREE","CAPABILITIES_DECLINE"],"mappings":";;;;;;;;;;AAyBAA,wBAAO,IAAI,iBACoC,CACvCC,cAAe,oBACfC,4BAA6B,kCAC7BC,2BAA4B,iCAC5BC,6BAA8B,mCAC9BC,4BAA6B,kCAC7BC,sBAAuB,4BACvBC,mBAAoB,mCACpBC,qBAAsB"}amd/build/tool_type.min.js000064400000002355152265520610011553 0ustar00/** * Provides an interface for a tool type in the Moodle server. * * @module mod_lti/tool_type * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/tool_type",["core/ajax","core/notification"],(function(ajax,notification){return{query:function(args){var request={methodname:"mod_lti_get_tool_types",args:args||{}},promise=ajax.call([request])[0];return promise.fail(notification.exception),promise},create:function(args){var request={methodname:"mod_lti_create_tool_type",args:args};return ajax.call([request])[0]},update:function(args){var request={methodname:"mod_lti_update_tool_type",args:args},promise=ajax.call([request])[0];return promise.fail(notification.exception),promise},delete:function(id){var request={methodname:"mod_lti_delete_tool_type",args:{id:id}},promise=ajax.call([request])[0];return promise.fail(notification.exception),promise},getFromToolProxyId:function(id){return this.query({toolproxyid:id})},isCartridge:function(url){var request={methodname:"mod_lti_is_cartridge",args:{url:url}};return ajax.call([request])[0]},constants:{state:{configured:1,pending:2,rejected:3}}}})); //# sourceMappingURL=tool_type.min.js.mapamd/build/form-field.min.js000064400000002416152265520610011557 0ustar00/** * A module that enables the setting of form field values on the client side. * * @module mod_lti/form-field * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define("mod_lti/form-field",["jquery"],(function($){var FormField=function(name,type,resetIfUndefined,defaultValue){this.name=name,this.id="id_"+this.name,this.selector="#"+this.id,this.type=type,this.resetIfUndefined=resetIfUndefined,this.defaultValue=defaultValue};return FormField.TYPES={TEXT:1,SELECT:2,CHECKBOX:3,EDITOR:4},FormField.prototype.setFieldValue=function(value){if(null===value){if(!this.resetIfUndefined)return;value=this.defaultValue}switch(this.type){case FormField.TYPES.CHECKBOX:value?$(this.selector).prop("checked",!0):$(this.selector).prop("checked",!1);break;case FormField.TYPES.EDITOR:if(void 0!==value.text){var attoEditor=$(this.selector+"editable");attoEditor.length?attoEditor.html(value.text):"undefined"!=typeof tinyMCE&&("3"==tinyMCE.majorVersion?tinyMCE.execInstanceCommand(this.id,"mceInsertContent",!1,value.text):tinyMCE.get(this.id).setContent(value.text)),$(this.selector).val(value.text)}break;default:$(this.selector).val(value)}},FormField})); //# sourceMappingURL=form-field.min.js.mapamd/build/form-field.min.js.map000064400000012623152265520610012334 0ustar00{"version":3,"file":"form-field.min.js","sources":["../src/form-field.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A module that enables the setting of form field values on the client side.\n *\n * @module mod_lti/form-field\n * @copyright 2016 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine(['jquery'],\n function($) {\n /**\n * Form field class.\n *\n * @param {string} name Field name.\n * @param {number} type The field type.\n * @param {boolean} resetIfUndefined Flag to reset the field to the default value if undefined in the return data.\n * @param {string|number|boolean} defaultValue The default value to use for the field.\n * @constructor\n */\n var FormField = function(name, type, resetIfUndefined, defaultValue) {\n this.name = name;\n this.id = 'id_' + this.name;\n this.selector = '#' + this.id;\n this.type = type;\n this.resetIfUndefined = resetIfUndefined;\n this.defaultValue = defaultValue;\n };\n\n /**\n * Form field types.\n *\n * @type {{TEXT: number, SELECT: number, CHECKBOX: number, EDITOR: number}}\n */\n FormField.TYPES = {\n TEXT: 1,\n SELECT: 2,\n CHECKBOX: 3,\n EDITOR: 4\n };\n\n /**\n * Sets the values for a form field.\n *\n * @param {string|boolean|number} value The value to be set into the field.\n */\n FormField.prototype.setFieldValue = function(value) {\n if (value === null) {\n if (this.resetIfUndefined) {\n value = this.defaultValue;\n } else {\n // No need set the field value if value is null and there's no need to reset the field.\n return;\n }\n }\n\n switch (this.type) {\n case FormField.TYPES.CHECKBOX:\n if (value) {\n $(this.selector).prop('checked', true);\n } else {\n $(this.selector).prop('checked', false);\n }\n break;\n case FormField.TYPES.EDITOR:\n if (typeof value.text !== 'undefined') {\n /* global tinyMCE:false */\n\n // Set text in editor's editable content, if applicable.\n // Check if it is an Atto editor.\n var attoEditor = $(this.selector + 'editable');\n if (attoEditor.length) {\n attoEditor.html(value.text);\n } else if (typeof tinyMCE !== 'undefined') {\n // If the editor is not Atto, try to fallback to TinyMCE.\n if (tinyMCE.majorVersion == \"3\") {\n // Tiny 3.\n tinyMCE.execInstanceCommand(this.id, 'mceInsertContent', false, value.text);\n } else {\n // Tiny 4+.\n tinyMCE.get(this.id).setContent(value.text);\n }\n }\n\n // Set text to actual editor text area.\n $(this.selector).val(value.text);\n }\n break;\n default:\n $(this.selector).val(value);\n break;\n }\n };\n\n return FormField;\n }\n);\n"],"names":["define","$","FormField","name","type","resetIfUndefined","defaultValue","id","this","selector","TYPES","TEXT","SELECT","CHECKBOX","EDITOR","prototype","setFieldValue","value","prop","text","attoEditor","length","html","tinyMCE","majorVersion","execInstanceCommand","get","setContent","val"],"mappings":";;;;;;;;AAuBAA,4BAAO,CAAC,WACJ,SAASC,OAUDC,UAAY,SAASC,KAAMC,KAAMC,iBAAkBC,mBAC9CH,KAAOA,UACPI,GAAK,MAAQC,KAAKL,UAClBM,SAAW,IAAMD,KAAKD,QACtBH,KAAOA,UACPC,iBAAmBA,sBACnBC,aAAeA,qBAQxBJ,UAAUQ,MAAQ,CACdC,KAAM,EACNC,OAAQ,EACRC,SAAU,EACVC,OAAQ,GAQZZ,UAAUa,UAAUC,cAAgB,SAASC,UAC3B,OAAVA,MAAgB,KACZT,KAAKH,wBACLY,MAAQT,KAAKF,oBAObE,KAAKJ,WACJF,UAAUQ,MAAMG,SACbI,MACAhB,EAAEO,KAAKC,UAAUS,KAAK,WAAW,GAEjCjB,EAAEO,KAAKC,UAAUS,KAAK,WAAW,cAGpChB,UAAUQ,MAAMI,eACS,IAAfG,MAAME,KAAsB,KAK/BC,WAAanB,EAAEO,KAAKC,SAAW,YAC/BW,WAAWC,OACXD,WAAWE,KAAKL,MAAME,MACI,oBAAZI,UAEc,KAAxBA,QAAQC,aAERD,QAAQE,oBAAoBjB,KAAKD,GAAI,oBAAoB,EAAOU,MAAME,MAGtEI,QAAQG,IAAIlB,KAAKD,IAAIoB,WAAWV,MAAME,OAK9ClB,EAAEO,KAAKC,UAAUmB,IAAIX,MAAME,oBAI/BlB,EAAEO,KAAKC,UAAUmB,IAAIX,SAK1Bf"}amd/build/tool_proxy.min.js.map000064400000007123152265520610012525 0ustar00{"version":3,"file":"tool_proxy.min.js","sources":["../src/tool_proxy.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Provides an interface for a tool proxy in the Moodle server.\n *\n * @module mod_lti/tool_proxy\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['core/ajax', 'core/notification'], function(ajax, notification) {\n return {\n /**\n * Get a list of tool types from Moodle for the given\n * search args.\n *\n * See also:\n * mod/lti/classes/external.php get_tool_types_parameters()\n *\n * @method query\n * @public\n * @param {Object} args Search parameters\n * @return {Promise} jQuery Deferred object\n */\n query: function(args) {\n var request = {\n methodname: 'mod_lti_get_tool_proxies',\n args: args || {}\n };\n\n var promise = ajax.call([request])[0];\n\n promise.fail(notification.exception);\n\n return promise;\n },\n /**\n * Delete a tool proxy from Moodle.\n *\n * @method delete\n * @public\n * @param {Integer} id Tool proxy ID\n * @return {Promise} jQuery Deferred object\n */\n 'delete': function(id) {\n var request = {\n methodname: 'mod_lti_delete_tool_proxy',\n args: {\n id: id\n }\n };\n\n var promise = ajax.call([request])[0];\n\n promise.fail(notification.exception);\n\n return promise;\n },\n\n /**\n * Create a tool proxy in Moodle.\n *\n * The promise will fail if the proxy cannot be created, so you must handle the fail result.\n *\n * See mod/lti/classes/external.php create_tool_proxy_parameters\n *\n * @method create\n * @public\n * @param {Object} args Tool proxy properties\n * @return {Promise} jQuery Deferred object\n */\n create: function(args) {\n var request = {\n methodname: 'mod_lti_create_tool_proxy',\n args: args\n };\n\n var promise = ajax.call([request])[0];\n\n return promise;\n }\n };\n});\n"],"names":["define","ajax","notification","query","args","request","methodname","promise","call","fail","exception","id","create"],"mappings":";;;;;;;;AAuBAA,4BAAO,CAAC,YAAa,sBAAsB,SAASC,KAAMC,oBAC/C,CAaHC,MAAO,SAASC,UACRC,QAAU,CACVC,WAAY,2BACZF,KAAMA,MAAQ,IAGdG,QAAUN,KAAKO,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKP,aAAaQ,WAEnBH,gBAUD,SAASI,QACXN,QAAU,CACVC,WAAY,4BACZF,KAAM,CACFO,GAAIA,KAIRJ,QAAUN,KAAKO,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKP,aAAaQ,WAEnBH,SAeXK,OAAQ,SAASR,UACTC,QAAU,CACVC,WAAY,4BACZF,KAAMA,aAGIH,KAAKO,KAAK,CAACH,UAAU"}amd/build/external_registration.min.js000064400000016126152265520610014152 0ustar00/** * Encapsules the behavior for creating a tool type and tool proxy from a * registration url in Moodle. * * Manages the UI while operations are occuring, including rendering external * registration page within the iframe. * * See template: mod_lti/external_registration * * @module mod_lti/external_registration * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/external_registration",["jquery","core/ajax","core/notification","core/templates","mod_lti/events","mod_lti/tool_proxy","mod_lti/tool_type","mod_lti/keys","core/str"],(function($,ajax,notification,templates,ltiEvents,toolProxy,toolType,KEYS,str){var SELECTORS_EXTERNAL_REGISTRATION_CONTAINER="#external-registration-page-container",SELECTORS_EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER="#external-registration-template-container",SELECTORS_EXTERNAL_REGISTRATION_CANCEL_BUTTON="#cancel-external-registration",SELECTORS_TOOL_TYPE_CAPABILITIES_CONTAINER="#tool-type-capabilities-container",SELECTORS_TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER="#tool-type-capabilities-template-container",SELECTORS_CAPABILITIES_AGREE_CONTAINER=".capabilities-container",getExternalRegistrationCancelButton=function(){return $(SELECTORS_EXTERNAL_REGISTRATION_CANCEL_BUTTON)},getExternalRegistrationContainer=function(){return $(SELECTORS_EXTERNAL_REGISTRATION_CONTAINER)},getExternalRegistrationTemplateContainer=function(){return $(SELECTORS_EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER)},getToolTypeCapabilitiesContainer=function(){return $(SELECTORS_TOOL_TYPE_CAPABILITIES_CONTAINER)},stopLoadingCancel=function(){getExternalRegistrationCancelButton().removeClass("loading")},hideExternalRegistrationContent=function(){getExternalRegistrationContainer().addClass("hidden")},getToolProxyId=function(){return getExternalRegistrationCancelButton().attr("data-tool-proxy-id")},hasToolProxyId=function(){return!!getToolProxyId()},hasCreatedToolProxy=function(){return getExternalRegistrationCancelButton().attr("data-tool-proxy-new")&&hasToolProxyId()},cancelRegistration=function(){getExternalRegistrationCancelButton().addClass("loading");var promise=$.Deferred();if(hasCreatedToolProxy()){var id=getToolProxyId();toolProxy.delete(id).done((function(){promise.resolve()})).fail((function(failure){promise.reject(failure)}))}else promise.resolve();return promise.done((function(){finishExternalRegistration(),stopLoadingCancel()})).fail((function(failure){notification.exception(failure),finishExternalRegistration(),stopLoadingCancel(),str.get_string("failedtodeletetoolproxy","mod_lti").done((function(s){var feedback={message:s,error:!0};$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,feedback)})).fail(notification.exception)})),promise},renderExternalRegistrationWindow=function(registrationRequest){var promise=templates.render("mod_lti/tool_proxy_registration_form",registrationRequest);return promise.done((function(html,js){var container=getExternalRegistrationTemplateContainer();container.append(html),templates.runTemplateJS(js),container.find("form").submit(),getExternalRegistrationContainer().removeClass("hidden")})).fail(notification.exception),promise},promptForToolTypeCapabilitiesAgreement=function(typeData){var promise=$.Deferred();return templates.render("mod_lti/tool_type_capabilities_agree",typeData).done((function(html,js){var container=$(SELECTORS_TOOL_TYPE_CAPABILITIES_TEMPLATE_CONTAINER);hideExternalRegistrationContent(),getToolTypeCapabilitiesContainer().removeClass("hidden"),templates.replaceNodeContents(container,html,js);var choiceContainer=container.find(SELECTORS_CAPABILITIES_AGREE_CONTAINER);choiceContainer.on(ltiEvents.CAPABILITIES_AGREE,(function(){getToolTypeCapabilitiesContainer().addClass("loading"),function(typeData){return toolType.update({id:typeData.id,state:toolType.constants.state.configured})}(typeData).always((function(){getToolTypeCapabilitiesContainer().removeClass("loading"),container.empty(),promise.resolve()}))})),choiceContainer.on(ltiEvents.CAPABILITIES_DECLINE,(function(){container.empty(),promise.resolve()}))})).fail(promise.reject),promise.done((function(){getToolTypeCapabilitiesContainer().addClass("hidden")})).fail(notification.exception),promise},createAndRegisterToolProxy=function(url){var promise=$.Deferred();return url&&""!==url?toolProxy.create({regurl:url}).done((function(result){getExternalRegistrationCancelButton().attr("data-tool-proxy-new","new"),promise=registerProxy(result.id)})).fail((function(exception){cancelRegistration();var feedback={message:exception.message,error:!0};$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,feedback),promise.reject(exception)})):promise.resolve(),promise},registerProxy=function(id){var promise=$.Deferred();return function(id){getExternalRegistrationCancelButton().attr("data-tool-proxy-id",id)}(id),function(id){var request={methodname:"mod_lti_get_tool_proxy_registration_request",args:{id:id}};return ajax.call([request])[0]}(id).done((function(registrationRequest){renderExternalRegistrationWindow(registrationRequest).done((function(){promise.resolve()})).fail(promise.fail)})).fail(promise.fail),promise},finishExternalRegistration=function(){hasToolProxyId()&&getExternalRegistrationCancelButton().removeAttr("data-tool-proxy-id"),getExternalRegistrationCancelButton().removeAttr("data-tool-proxy-new"),hideExternalRegistrationContent(),getExternalRegistrationTemplateContainer().empty(),$(document).trigger(ltiEvents.STOP_EXTERNAL_REGISTRATION)};return{init:function(){!function(){$(document).on(ltiEvents.START_EXTERNAL_REGISTRATION,(function(event,data){data&&(data.url&&createAndRegisterToolProxy(data.url),data.proxyid&®isterProxy(data.proxyid))}));var cancelExternalRegistrationButton=getExternalRegistrationCancelButton();cancelExternalRegistrationButton.click((function(e){e.preventDefault(),cancelRegistration()})),cancelExternalRegistrationButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),cancelRegistration())})),window.triggerExternalRegistrationComplete=function(data){var promise=$.Deferred(),feedback={message:"",error:!1};if("success"==data.status){if(str.get_string("successfullycreatedtooltype","mod_lti").done((function(s){feedback.message=s})).fail(notification.exception),promise.done((function(){finishExternalRegistration(),$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,feedback),$(document).trigger(ltiEvents.NEW_TOOL_TYPE)})).fail(notification.exception),hasCreatedToolProxy()){var proxyId=getToolProxyId();toolType.getFromToolProxyId(proxyId).done((function(types){if(types&&types.length){var typeData=types[0];typeData.hascapabilitygroups?promptForToolTypeCapabilitiesAgreement(typeData).always((function(){promise.resolve()})):promise.resolve()}else promise.resolve()})).fail((function(){promise.resolve()}))}}else feedback.message=data.error,feedback.error=!0,promise.done((function(){cancelRegistration().always((function(){$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,feedback)}))})).fail(notification.exception),promise.resolve();return promise}}()}}})); //# sourceMappingURL=external_registration.min.js.mapamd/build/contentitem_return.min.js.map000064400000003744152265520610014244 0ustar00{"version":3,"file":"contentitem_return.min.js","sources":["../src/contentitem_return.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Processes the result of LTI tool creation from a Content-Item message type.\n *\n * @module mod_lti/contentitem_return\n * @copyright 2016 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine(['jquery'], function($) {\n return {\n /**\n * Init function.\n *\n * @param {string} returnData The returned data.\n */\n init: function(returnData) {\n // Make sure the window has loaded before we perform processing.\n $(window).ready(function() {\n if (window != top) {\n // Send return data to be processed by the parent window.\n parent.processContentItemReturnData(returnData);\n } else {\n window.processContentItemReturnData(returnData);\n }\n });\n }\n };\n});\n"],"names":["define","$","init","returnData","window","ready","top","parent","processContentItemReturnData"],"mappings":";;;;;;;;AAuBAA,oCAAO,CAAC,WAAW,SAASC,SACjB,CAMHC,KAAM,SAASC,YAEXF,EAAEG,QAAQC,OAAM,WACRD,QAAUE,IAEVC,OAAOC,6BAA6BL,YAEpCC,OAAOI,6BAA6BL"}amd/build/events.min.js000064400000001464152265520610011041 0ustar00/** * Provides a list of events that can be triggered in the LTI management * page. * * @module mod_lti/events * @class events * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/events",[],(function(){return{NEW_TOOL_TYPE:"lti.tool.type.new",START_EXTERNAL_REGISTRATION:"lti.registration.external.start",STOP_EXTERNAL_REGISTRATION:"lti.registration.external.stop",START_CARTRIDGE_REGISTRATION:"lti.registration.cartridge.start",STOP_CARTRIDGE_REGISTRATION:"lti.registration.cartridge.stop",REGISTRATION_FEEDBACK:"lti.registration.feedback",CAPABILITIES_AGREE:"lti.tool.type.capabilities.agree",CAPABILITIES_DECLINE:"lti.tool.type.capabilities.decline"}})); //# sourceMappingURL=events.min.js.mapamd/build/tool_proxy_card_controller.min.js000064400000006203152265520610015203 0ustar00/** * Controls all of the behaviour and interaction with a tool type card. These are * listed on the LTI tool type management page. * * See template: mod_lti/tool_proxy_card * * @module mod_lti/tool_proxy_card_controller * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/tool_proxy_card_controller",["jquery","core/ajax","core/notification","core/templates","mod_lti/tool_proxy","mod_lti/events","mod_lti/keys","core/str"],(function($,ajax,notification,templates,toolProxy,ltiEvents,KEYS,str){var SELECTORS_DELETE_BUTTON=".delete",SELECTORS_ACTIVATE_BUTTON=".tool-card-footer a.activate",getTypeId=function(element){return element.attr("data-proxy-id")},clearAllAnnouncements=function(element){element.removeClass("announcement loading success fail capabilities")},stopLoading=function(element){element.removeClass("announcement loading")},deleteType=function(element){var promise=$.Deferred(),typeId=getTypeId(element);return function(element){clearAllAnnouncements(element),element.addClass("announcement loading")}(element),""===typeId?$.Deferred().resolve():(str.get_strings([{key:"delete",component:"mod_lti"},{key:"delete_confirmation",component:"mod_lti"},{key:"delete",component:"mod_lti"},{key:"cancel",component:"core"}]).done((function(strs){notification.confirm(strs[0],strs[1],strs[2],strs[3],(function(){toolProxy.delete(typeId).done((function(){stopLoading(element),function(element){var promise=$.Deferred();return clearAllAnnouncements(element),element.addClass("announcement success"),setTimeout((function(){element.removeClass("announcement success"),promise.resolve()}),2e3),promise}(element).done((function(){element.remove(),promise.resolve()})).fail(notification.exception)})).fail((function(error){!function(element){var promise=$.Deferred();clearAllAnnouncements(element),element.addClass("announcement fail"),setTimeout((function(){element.removeClass("announcement fail"),promise.resolve()}),2e3)}(element),promise.reject(error)}))}),(function(){stopLoading(element),promise.resolve()}))})).fail((function(error){stopLoading(element),notification.exception(error),promise.reject(error)})),promise)},registerEventListeners=function(element){var deleteButton=function(element){return element.find(SELECTORS_DELETE_BUTTON)}(element);deleteButton.click((function(e){e.preventDefault(),deleteType(element)})),deleteButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),deleteButton.click())}));var activateButton=function(element){return element.find(SELECTORS_ACTIVATE_BUTTON)}(element);activateButton.click((function(e){e.preventDefault(),function(element){var data={proxyid:getTypeId(element)};$(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION,data)}(element)})),activateButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),activateButton.click())}))};return{init:function(element){registerEventListeners(element)}}})); //# sourceMappingURL=tool_proxy_card_controller.min.js.mapamd/build/keys.min.js000064400000000567152265520610010513 0ustar00/** * A list of keys and their keycodes that are used by the LTI modules. * * @module mod_lti/keys * @class keys * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/keys",[],(function(){return{ENTER:13,SPACE:32}})); //# sourceMappingURL=keys.min.js.mapamd/build/course_tools_list.min.js000064400000006677152265520610013323 0ustar00define("mod_lti/course_tools_list",["exports","core/notification","core/pending","core/ajax","core/toast","core/str","core_table/dynamic","core_table/local/dynamic/selectors","./repository"],(function(_exports,_notification,_pending,_ajax,_toast,_str,_dynamic,Selectors,_repository){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_ajax=_interopRequireDefault(_ajax),Selectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Selectors);_exports.init=()=>{document.addEventListener("click",(event=>{const courseToolDelete=event.target.closest('[data-action="course-tool-delete"]');if(courseToolDelete){event.preventDefault();const deleteBodyStringId=courseToolDelete.dataset.courseToolUsage>0?"deletecoursetoolwithusageconfirm":"deletecoursetoolconfirm",requiredStrings=[{key:"deletecoursetool",component:"mod_lti",param:courseToolDelete.dataset.courseToolName},{key:deleteBodyStringId,component:"mod_lti",param:courseToolDelete.dataset.courseToolName},{key:"delete",component:"core",param:courseToolDelete.dataset.courseToolName},{key:"coursetooldeleted",component:"mod_lti",param:courseToolDelete.dataset.courseToolName}],triggerElement=courseToolDelete.closest(".dropdown").querySelector(".dropdown-toggle");(0,_str.getStrings)(requiredStrings).then((_ref=>{let[modalTitle,modalBody,deleteLabel]=_ref;return _notification.default.deleteCancelPromise(modalTitle,modalBody,deleteLabel,{triggerElement:triggerElement})})).then((()=>{const pendingPromise=new _pending.default("mod_lti/course_tools:delete"),request={methodname:"mod_lti_delete_course_tool_type",args:{tooltypeid:courseToolDelete.dataset.courseToolId}};return _ajax.default.call([request])[0].then((0,_toast.add)((0,_str.getString)("coursetooldeleted","mod_lti",courseToolDelete.dataset.courseToolName))).then((()=>{const tableRoot=triggerElement.closest(Selectors.main.region);return(0,_dynamic.refreshTableContent)(tableRoot)})).then(pendingPromise.resolve).catch(_notification.default.exception)})).catch((()=>{}))}const courseShowInActivityChooser=event.target.closest('[data-action="showinactivitychooser-toggle"]');if(courseShowInActivityChooser){const showInActivityChooserStateToggle="0"===courseShowInActivityChooser.dataset.state?1:0;return(0,_repository.toggleShowInActivityChooser)(courseShowInActivityChooser.dataset.id,courseShowInActivityChooser.dataset.courseid,showInActivityChooserStateToggle)}}))}})); //# sourceMappingURL=course_tools_list.min.js.mapamd/build/tool_proxy_card_controller.min.js.map000064400000030020152265520610015751 0ustar00{"version":3,"file":"tool_proxy_card_controller.min.js","sources":["../src/tool_proxy_card_controller.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls all of the behaviour and interaction with a tool type card. These are\n * listed on the LTI tool type management page.\n *\n * See template: mod_lti/tool_proxy_card\n *\n * @module mod_lti/tool_proxy_card_controller\n * @copyright 2016 John Okely \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/tool_proxy', 'mod_lti/events', 'mod_lti/keys',\n 'core/str'],\n function($, ajax, notification, templates, toolProxy, ltiEvents, KEYS, str) {\n\n var SELECTORS = {\n DELETE_BUTTON: '.delete',\n CAPABILITIES_CONTAINER: '.capabilities-container',\n ACTIVATE_BUTTON: '.tool-card-footer a.activate',\n };\n\n // Timeout in seconds.\n var ANNOUNCEMENT_TIMEOUT = 2000;\n\n /**\n * Return the delete button element.\n *\n * @method getDeleteButton\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {JQuery} jQuery object\n */\n var getDeleteButton = function(element) {\n return element.find(SELECTORS.DELETE_BUTTON);\n };\n\n /**\n * Return the activate button for the type.\n *\n * @method getActivateButton\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {JQuery} jQuery object\n */\n var getActivateButton = function(element) {\n return element.find(SELECTORS.ACTIVATE_BUTTON);\n };\n\n /**\n * Get the type id.\n *\n * @method getTypeId\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {String} Type ID\n */\n var getTypeId = function(element) {\n return element.attr('data-proxy-id');\n };\n\n /**\n * Stop any announcement currently visible on the card.\n *\n * @method clearAllAnnouncements\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var clearAllAnnouncements = function(element) {\n element.removeClass('announcement loading success fail capabilities');\n };\n\n /**\n * Show the loading announcement.\n *\n * @method startLoading\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var startLoading = function(element) {\n clearAllAnnouncements(element);\n element.addClass('announcement loading');\n };\n\n /**\n * Hide the loading announcement.\n *\n * @method stopLoading\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var stopLoading = function(element) {\n element.removeClass('announcement loading');\n };\n\n /**\n * Show the success announcement. The announcement is only\n * visible for 2 seconds.\n *\n * @method announceSuccess\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var announceSuccess = function(element) {\n var promise = $.Deferred();\n\n clearAllAnnouncements(element);\n element.addClass('announcement success');\n setTimeout(function() {\n element.removeClass('announcement success');\n promise.resolve();\n }, ANNOUNCEMENT_TIMEOUT);\n\n return promise;\n };\n\n /**\n * Show the failure announcement. The announcement is only\n * visible for 2 seconds.\n *\n * @method announceFailure\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var announceFailure = function(element) {\n var promise = $.Deferred();\n\n clearAllAnnouncements(element);\n element.addClass('announcement fail');\n setTimeout(function() {\n element.removeClass('announcement fail');\n promise.resolve();\n }, ANNOUNCEMENT_TIMEOUT);\n\n return promise;\n };\n\n /**\n * Delete the tool type from the Moodle server. Triggers a success\n * or failure announcement depending on the result.\n *\n * @method deleteType\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var deleteType = function(element) {\n var promise = $.Deferred();\n var typeId = getTypeId(element);\n startLoading(element);\n\n if (typeId === \"\") {\n return $.Deferred().resolve();\n }\n\n str.get_strings([\n {\n key: 'delete',\n component: 'mod_lti'\n },\n {\n key: 'delete_confirmation',\n component: 'mod_lti'\n },\n {\n key: 'delete',\n component: 'mod_lti'\n },\n {\n key: 'cancel',\n component: 'core'\n },\n ])\n .done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n toolProxy.delete(typeId)\n .done(function() {\n stopLoading(element);\n announceSuccess(element)\n .done(function() {\n element.remove();\n promise.resolve();\n })\n .fail(notification.exception);\n })\n .fail(function(error) {\n announceFailure(element);\n promise.reject(error);\n });\n }, function() {\n stopLoading(element);\n promise.resolve();\n });\n })\n .fail(function(error) {\n stopLoading(element);\n notification.exception(error);\n promise.reject(error);\n });\n\n return promise;\n };\n\n /**\n * The user wishes to activate this tool so show them the capabilities that\n * they need to agree to or if there are none then set the tool type's state\n * to active.\n *\n * @method activateToolType\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var activateToolType = function(element) {\n var data = {proxyid: getTypeId(element)};\n $(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION, data);\n };\n\n /**\n * Sets up the listeners for user interaction on this tool type card.\n *\n * @method registerEventListeners\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var registerEventListeners = function(element) {\n var deleteButton = getDeleteButton(element);\n deleteButton.click(function(e) {\n e.preventDefault();\n deleteType(element);\n });\n deleteButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n deleteButton.click();\n }\n }\n });\n\n var activateButton = getActivateButton(element);\n activateButton.click(function(e) {\n e.preventDefault();\n activateToolType(element);\n });\n activateButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n activateButton.click();\n }\n }\n });\n };\n\n return /** @alias module:mod_lti/tool_card_controller */ {\n\n /**\n * Initialise this module.\n *\n * @param {JQuery} element jQuery object representing the tool card.\n */\n init: function(element) {\n registerEventListeners(element);\n }\n };\n});\n"],"names":["define","$","ajax","notification","templates","toolProxy","ltiEvents","KEYS","str","SELECTORS","getTypeId","element","attr","clearAllAnnouncements","removeClass","stopLoading","deleteType","promise","Deferred","typeId","addClass","startLoading","resolve","get_strings","key","component","done","strs","confirm","delete","setTimeout","announceSuccess","remove","fail","exception","error","announceFailure","reject","registerEventListeners","deleteButton","find","getDeleteButton","click","e","preventDefault","keypress","metaKey","shiftKey","altKey","ctrlKey","keyCode","ENTER","SPACE","activateButton","getActivateButton","data","proxyid","document","trigger","START_EXTERNAL_REGISTRATION","activateToolType","init"],"mappings":";;;;;;;;;;;AA0BAA,4CAAO,CAAC,SAAU,YAAa,oBAAqB,iBAAkB,qBAAsB,iBAAkB,eACtG,aACA,SAASC,EAAGC,KAAMC,aAAcC,UAAWC,UAAWC,UAAWC,KAAMC,SAEvEC,wBACe,UADfA,0BAGiB,+BAsCjBC,UAAY,SAASC,gBACdA,QAAQC,KAAK,kBAUpBC,sBAAwB,SAASF,SACjCA,QAAQG,YAAY,mDAsBpBC,YAAc,SAASJ,SACvBA,QAAQG,YAAY,yBAwDpBE,WAAa,SAASL,aAClBM,QAAUhB,EAAEiB,WACZC,OAAST,UAAUC,gBAvER,SAASA,SACxBE,sBAAsBF,SACtBA,QAAQS,SAAS,wBAsEjBC,CAAaV,SAEE,KAAXQ,OACOlB,EAAEiB,WAAWI,WAGxBd,IAAIe,YAAY,CACR,CACIC,IAAK,SACLC,UAAW,WAEf,CACID,IAAK,sBACLC,UAAW,WAEf,CACID,IAAK,SACLC,UAAW,WAEf,CACID,IAAK,SACLC,UAAW,UAGlBC,MAAK,SAASC,MACPxB,aAAayB,QAAQD,KAAK,GAAIA,KAAK,GAAIA,KAAK,GAAIA,KAAK,IAAI,WACjDtB,UAAUwB,OAAOV,QACZO,MAAK,WACEX,YAAYJ,SA3E1B,SAASA,aACvBM,QAAUhB,EAAEiB,kBAEhBL,sBAAsBF,SACtBA,QAAQS,SAAS,wBACjBU,YAAW,WACPnB,QAAQG,YAAY,wBACpBG,QAAQK,YAxFW,KA2FhBL,QAkEyBc,CAAgBpB,SACXe,MAAK,WACEf,QAAQqB,SACRf,QAAQK,aAEfW,KAAK9B,aAAa+B,cAE9BD,MAAK,SAASE,QA7DrB,SAASxB,aACvBM,QAAUhB,EAAEiB,WAEhBL,sBAAsBF,SACtBA,QAAQS,SAAS,qBACjBU,YAAW,WACHnB,QAAQG,YAAY,qBACpBG,QAAQK,YA9GO,KAqKSc,CAAgBzB,SAChBM,QAAQoB,OAAOF,aAEhC,WACKpB,YAAYJ,SACZM,QAAQK,gBAGvBW,MAAK,SAASE,OACPpB,YAAYJ,SACZR,aAAa+B,UAAUC,OACvBlB,QAAQoB,OAAOF,UAGpBlB,UAwBPqB,uBAAyB,SAAS3B,aAC9B4B,aAlMc,SAAS5B,gBACpBA,QAAQ6B,KAAK/B,yBAiMDgC,CAAgB9B,SACnC4B,aAAaG,OAAM,SAASC,GACpBA,EAAEC,iBACF5B,WAAWL,YAEnB4B,aAAaM,UAAS,SAASF,GAClBA,EAAEG,SAAYH,EAAEI,UAAaJ,EAAEK,QAAWL,EAAEM,SACzCN,EAAEO,SAAW3C,KAAK4C,OAASR,EAAEO,SAAW3C,KAAK6C,QAC7CT,EAAEC,iBACFL,aAAaG,gBAKzBW,eApMgB,SAAS1C,gBACtBA,QAAQ6B,KAAK/B,2BAmMC6C,CAAkB3C,SACvC0C,eAAeX,OAAM,SAASC,GACtBA,EAAEC,iBA7BS,SAASjC,aACxB4C,KAAO,CAACC,QAAS9C,UAAUC,UAC/BV,EAAEwD,UAAUC,QAAQpD,UAAUqD,4BAA6BJ,MA4BnDK,CAAiBjD,YAEzB0C,eAAeR,UAAS,SAASF,GACpBA,EAAEG,SAAYH,EAAEI,UAAaJ,EAAEK,QAAWL,EAAEM,SACzCN,EAAEO,SAAW3C,KAAK4C,OAASR,EAAEO,SAAW3C,KAAK6C,QAC7CT,EAAEC,iBACFS,eAAeX,mBAMsB,CAOrDmB,KAAM,SAASlD,SACX2B,uBAAuB3B"}amd/build/coursecategory.min.js000064400000003610152265520610012566 0ustar00/** * Select course categories for LTI tool. * * @module mod_lti/coursecategory * @copyright 2023 Jackson D'souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.3 */ define("mod_lti/coursecategory",[],(function(){function getParents(elem){const parents=[];for(;elem&&elem!==document;elem=elem.parentNode)elem.classList.contains("accordion-group")&&parents.push(elem);return parents}return document.addEventListener("click",(event=>{const checkedbox=event.target.closest(".lticoursecategories");if(checkedbox){const checkboxstatus=checkedbox.checked,categorycontainer=document.querySelector("#collapse"+checkedbox.value);if(categorycontainer){const categorycontainercheckbox=categorycontainer.querySelectorAll('input[type="checkbox"]');for(let i=0;i.\n\n/**\n * Provides an interface for external tools in the Moodle server.\n *\n * @module mod_lti/tool_types_and_proxies\n * @class tool_types_and_proxies\n * @copyright 2020 Andrew Madden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\nimport ajax from 'core/ajax';\n\n/**\n * Get a list of LTI tool types and tool proxies from Moodle for the given\n * search args.\n *\n * See also:\n * mod/lti/classes/external.php get_tool_types_and_proxies()\n *\n * @method query\n * @public\n * @param {Object} args Search parameters\n * @return {Promise} Promise that will be resolved when the ajax call returns.\n */\nexport const query = (args) => {\n const request = {\n methodname: 'mod_lti_get_tool_types_and_proxies',\n args: args || {}\n };\n\n return ajax.call([request])[0];\n};\n\n/**\n * Get a count of LTI tool types and tool proxies from Moodle for the given\n * search args.\n *\n * See also:\n * mod/lti/classes/external.php get_tool_types_and_proxies_count()\n *\n * @method count\n * @public\n * @param {Object} args Search parameters\n * @return {Promise} Promise that will be resolved when the ajax call returns.\n */\nexport const count = (args) => {\n const request = {\n methodname: 'mod_lti_get_tool_types_and_proxies_count',\n args: args || {}\n };\n\n return ajax.call([request])[0];\n};\n"],"names":["args","request","methodname","ajax","call"],"mappings":";;;;;;;;;qKAsCsBA,aACZC,QAAU,CACZC,WAAY,qCACZF,KAAMA,MAAQ,WAGXG,cAAKC,KAAK,CAACH,UAAU,mBAeVD,aACZC,QAAU,CACZC,WAAY,2CACZF,KAAMA,MAAQ,WAGXG,cAAKC,KAAK,CAACH,UAAU"}amd/build/mod_form.min.js.map000064400000010125152265520610012105 0ustar00{"version":3,"file":"mod_form.min.js","sources":["../src/mod_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Event handlers for the mod_lti mod_form.\n *\n * @module mod_lti/mod_form\n * @copyright 2023 Jake Dallimore \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport ContentItem from 'mod_lti/contentitem';\n\n/**\n * Initialise module.\n *\n * @param {int} courseId the course id.\n * @param {string} toolUrl the external tool url.\n */\nconst init = (courseId, toolUrl) => {\n const contentItemButton = document.querySelector('[name=\"selectcontent\"]');\n\n if (!contentItemButton) {\n return;\n }\n\n contentItemButton.addEventListener('click', () => {\n const contentItemUrl = contentItemButton.getAttribute('data-contentitemurl');\n const contentItemId = document.querySelector('#hidden_typeid').value;\n if (contentItemId) {\n const title = document.querySelector('#id_name').value.trim();\n const text = document.querySelector('#id_introeditor').value.trim();\n const postData = {\n id: contentItemId,\n course: courseId,\n title: title,\n text: text\n };\n\n // The callback below is called after the content item has been returned and processed.\n ContentItem.init(contentItemUrl, toolUrl, postData, (returnData) => {\n if (!returnData.multiple) {\n // The state of the grade checkbox has already been set by processContentItemReturnData() but that\n // hasn't fired the click/change event required by formslib to show/hide the dependent grade fields.\n // Fire it now.\n const allowGrades = document.querySelector('#id_instructorchoiceacceptgrades');\n let allowGradesChangeEvent = new Event('change');\n allowGrades.dispatchEvent(allowGradesChangeEvent);\n\n // If the tool is set to accept grades, make sure \"Point\" is selected.\n if (allowGrades.checked) {\n const gradeType = document.querySelector('#id_grade_modgrade_type');\n gradeType.value = \"point\";\n let gradeTypeChangeEvent = new Event('change');\n gradeType.dispatchEvent(gradeTypeChangeEvent);\n }\n }\n });\n }\n });\n};\n\nexport default {\n init: init\n};\n"],"names":["init","courseId","toolUrl","contentItemButton","document","querySelector","addEventListener","contentItemUrl","getAttribute","contentItemId","value","title","trim","text","postData","id","course","returnData","multiple","allowGrades","allowGradesChangeEvent","Event","dispatchEvent","checked","gradeType","gradeTypeChangeEvent"],"mappings":"oQA4Ee,CACXA,KA5CS,CAACC,SAAUC,iBACdC,kBAAoBC,SAASC,cAAc,0BAE5CF,mBAILA,kBAAkBG,iBAAiB,SAAS,WAClCC,eAAiBJ,kBAAkBK,aAAa,uBAChDC,cAAgBL,SAASC,cAAc,kBAAkBK,SAC3DD,cAAe,OACTE,MAAQP,SAASC,cAAc,YAAYK,MAAME,OACjDC,KAAOT,SAASC,cAAc,mBAAmBK,MAAME,OACvDE,SAAW,CACbC,GAAIN,cACJO,OAAQf,SACRU,MAAOA,MACPE,KAAMA,2BAIEb,KAAKO,eAAgBL,QAASY,UAAWG,iBAC5CA,WAAWC,SAAU,OAIhBC,YAAcf,SAASC,cAAc,wCACvCe,uBAAyB,IAAIC,MAAM,aACvCF,YAAYG,cAAcF,wBAGtBD,YAAYI,QAAS,OACfC,UAAYpB,SAASC,cAAc,2BACzCmB,UAAUd,MAAQ,YACde,qBAAuB,IAAIJ,MAAM,UACrCG,UAAUF,cAAcG"}amd/build/tool_card_controller.min.js000064400000021151152265520610013741 0ustar00/** * Controls all of the behaviour and interaction with a tool type card. These are * listed on the LTI tool type management page. * * See template: mod_lti/tool_card * * @module mod_lti/tool_card_controller * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/tool_card_controller",["jquery","core/ajax","core/notification","core/templates","core/modal","mod_lti/tool_type","mod_lti/events","mod_lti/keys","core/str"],(function($,ajax,notification,templates,Modal,toolType,ltiEvents,KEYS,str){var SELECTORS_DELETE_BUTTON=".delete",SELECTORS_NAME_ELEMENT=".name",SELECTORS_DESCRIPTION_ELEMENT=".description",SELECTORS_CAPABILITIES_CONTAINER=".capabilities-container",SELECTORS_ACTIVATE_BUTTON=".tool-card-footer a.activate",getNameElement=function(element){return element.find(SELECTORS_NAME_ELEMENT)},getDescriptionElement=function(element){return element.find(SELECTORS_DESCRIPTION_ELEMENT)},getActivateButton=function(element){return element.find(SELECTORS_ACTIVATE_BUTTON)},getCapabilitiesContainer=function(element){return element.find(SELECTORS_CAPABILITIES_CONTAINER)},hasCapabilitiesContainer=function(element){return!!getCapabilitiesContainer(element).length},getTypeId=function(element){return element.attr("data-type-id")},clearAllAnnouncements=function(element){element.removeClass("announcement loading success fail capabilities")},startLoading=function(element){clearAllAnnouncements(element),element.addClass("announcement loading")},stopLoading=function(element){element.removeClass("announcement loading")},announceSuccess=function(element){var promise=$.Deferred();return clearAllAnnouncements(element),element.addClass("announcement success"),setTimeout((function(){element.removeClass("announcement success"),promise.resolve()}),2e3),promise},announceFailure=function(element){var promise=$.Deferred();return clearAllAnnouncements(element),element.addClass("announcement fail"),setTimeout((function(){element.removeClass("announcement fail"),promise.resolve()}),2e3),promise},setValueSnapshot=function(element,value){element.attr("data-val-snapshot",value)},getValueSnapshot=function(element){return element.attr("data-val-snapshot")},setStatusActive=function(element){var id=getTypeId(element);if(""===id)return $.Deferred().resolve();startLoading(element);var promise=toolType.update({id:id,state:toolType.constants.state.configured});return promise.then((function(toolTypeData){return stopLoading(element),announceSuccess(element),toolTypeData})).then((function(toolTypeData){return templates.render("mod_lti/tool_card",toolTypeData)})).then((function(html,js){templates.replaceNode(element,html,js)})).catch((function(){stopLoading(element),announceFailure(element)})),promise},registerEventListeners=function(element){var deleteButton=function(element){return element.find(SELECTORS_DELETE_BUTTON)}(element);deleteButton.click((function(e){e.preventDefault(),function(element){var promise=$.Deferred(),typeId=getTypeId(element);startLoading(element),""===typeId?$.Deferred().resolve():str.get_strings([{key:"delete",component:"mod_lti"},{key:"delete_confirmation",component:"mod_lti"},{key:"delete",component:"mod_lti"},{key:"cancel",component:"core"}]).done((function(strs){notification.confirm(strs[0],strs[1],strs[2],strs[3],(function(){toolType.delete(typeId).done((function(){stopLoading(element),announceSuccess(element).done((function(){element.remove()})).fail(notification.exception).always((function(){promise.resolve()}))})).fail((function(error){announceFailure(element),promise.reject(error)}))}),(function(){stopLoading(element),promise.resolve()}))})).fail((function(error){stopLoading(element),notification.exception(error),promise.reject(error)}))}(element)})),deleteButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),deleteButton.click())}));var descriptionElement=getDescriptionElement(element);descriptionElement.focus((function(e){e.preventDefault(),function(element){var descriptionElement=getDescriptionElement(element);if(!descriptionElement.hasClass("loading")){var description=descriptionElement.text().trim();setValueSnapshot(descriptionElement,description)}}(element)})),descriptionElement.blur((function(e){e.preventDefault(),function(element){var typeId=getTypeId(element);if(""===typeId)return $.Deferred().resolve();var descriptionElement=getDescriptionElement(element);if(descriptionElement.hasClass("loading"))return $.Deferred().resolve();var description=descriptionElement.text().trim();if(getValueSnapshot(descriptionElement)==description)return $.Deferred().resolve();descriptionElement.addClass("loading");var promise=toolType.update({id:typeId,description:description});promise.done((function(type){descriptionElement.removeClass("loading"),descriptionElement.text(type.description)})).fail(notification.exception),promise.fail((function(){descriptionElement.removeClass("loading")}))}(element)})),descriptionElement.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode==KEYS.ENTER&&(e.preventDefault(),descriptionElement.blur())}));var nameElement=getNameElement(element);if(nameElement.focus((function(e){e.preventDefault(),function(element){var nameElement=getNameElement(element);if(!nameElement.hasClass("loading")){var name=nameElement.text().trim();setValueSnapshot(nameElement,name)}}(element)})),nameElement.blur((function(e){e.preventDefault(),function(element){var typeId=getTypeId(element);if(""===typeId)return $.Deferred().resolve();var nameElement=getNameElement(element);if(nameElement.hasClass("loading"))return $.Deferred().resolve();var name=nameElement.text().trim();if(getValueSnapshot(nameElement)==name)return $.Deferred().resolve();nameElement.addClass("loading");var promise=toolType.update({id:typeId,name:name});promise.done((function(type){nameElement.removeClass("loading"),nameElement.text(type.name)})),promise.fail((function(){nameElement.removeClass("loading")}))}(element)})),nameElement.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode==KEYS.ENTER&&(e.preventDefault(),nameElement.blur())})),function(element){return!!getActivateButton(element).length}(element)){var activateButton=getActivateButton(element);activateButton.click((function(e){e.preventDefault(),function(element){hasCapabilitiesContainer(element)?function(element){element.addClass("announcement capabilities")}(element):setStatusActive(element)}(element)})),activateButton.keypress((function(e){e.metaKey||e.shiftKey||e.altKey||e.ctrlKey||e.keyCode!=KEYS.ENTER&&e.keyCode!=KEYS.SPACE||(e.preventDefault(),activateButton.click())}))}if(hasCapabilitiesContainer(element)){var capabilitiesContainer=getCapabilitiesContainer(element);capabilitiesContainer.on(ltiEvents.CAPABILITIES_AGREE,(function(){setStatusActive(element)})),capabilitiesContainer.on(ltiEvents.CAPABILITIES_DECLINE,(function(){!function(element){element.removeClass("announcement capabilities")}(element)}))}};return{init:function(element){registerEventListeners(element),function(element){const configurationLink=element.find("#"+element.data("uniqid")+"-"+element.data("deploymentid"));if(!configurationLink.length)return;configurationLink.get(0).addEventListener("click",(e=>{e.preventDefault();var context={uniqid:element.data("uniqid"),platformid:element.data("platformid"),clientid:element.data("clientid"),deploymentid:element.data("deploymentid"),urls:{publickeyset:element.data("publickeyseturl"),accesstoken:element.data("accesstokenurl"),authrequest:element.data("authrequesturl")}},bodyPromise=templates.render("mod_lti/tool_config_modal_body",context);context={mailto:"mailto:?subject="+encodeURIComponent(element.data("mailtosubject"))+"&body="+encodeURIComponent(element.data("platformidstr"))+":%20"+encodeURIComponent(element.data("platformid"))+"%0D%0A"+encodeURIComponent(element.data("clientidstr"))+":%20"+encodeURIComponent(element.data("clientid"))+"%0D%0A"+encodeURIComponent(element.data("deploymentidstr"))+":%20"+encodeURIComponent(element.data("deploymentid"))+"%0D%0A"+encodeURIComponent(element.data("publickeyseturlstr"))+":%20"+encodeURIComponent(element.data("publickeyseturl"))+"%0D%0A"+encodeURIComponent(element.data("accesstokenurlstr"))+":%20"+encodeURIComponent(element.data("accesstokenurl"))+"%0D%0A"+encodeURIComponent(element.data("authrequesturlstr"))+":%20"+encodeURIComponent(element.data("authrequesturl"))+"%0D%0A"};var footerPromise=templates.render("mod_lti/tool_config_modal_footer",context);Modal.create({large:!0,title:element.data("modaltitle"),body:bodyPromise,footer:footerPromise,show:!0})}))}(element)}}})); //# sourceMappingURL=tool_card_controller.min.js.mapamd/build/course_tools_list.min.js.map000064400000013566152265520610014072 0ustar00{"version":3,"file":"course_tools_list.min.js","sources":["../src/course_tools_list.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course LTI External tools list management.\n *\n * @module mod_lti/course_tools_list\n * @copyright 2023 Jake Dallimore \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Ajax from 'core/ajax';\nimport {add as addToast} from 'core/toast';\nimport {getString, getStrings} from 'core/str';\nimport {refreshTableContent} from 'core_table/dynamic';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\nimport {toggleShowInActivityChooser} from \"./repository\";\n\n/**\n * Initialise module.\n */\nexport const init = () => {\n document.addEventListener('click', event => {\n\n const courseToolDelete = event.target.closest('[data-action=\"course-tool-delete\"]');\n if (courseToolDelete) {\n event.preventDefault();\n\n // A different message is used in the modal if the tool has usages within the course.\n const usage = courseToolDelete.dataset.courseToolUsage;\n const deleteBodyStringId = usage > 0 ? 'deletecoursetoolwithusageconfirm' : 'deletecoursetoolconfirm';\n const requiredStrings = [\n {key: 'deletecoursetool', component: 'mod_lti', param: courseToolDelete.dataset.courseToolName},\n {key: deleteBodyStringId, component: 'mod_lti', param: courseToolDelete.dataset.courseToolName},\n {key: 'delete', component: 'core', param: courseToolDelete.dataset.courseToolName},\n {key: 'coursetooldeleted', component: 'mod_lti', param: courseToolDelete.dataset.courseToolName}\n ];\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = courseToolDelete.closest('.dropdown').querySelector('.dropdown-toggle');\n\n getStrings(requiredStrings).then(([modalTitle, modalBody, deleteLabel]) => {\n return Notification.deleteCancelPromise(\n modalTitle,\n modalBody,\n deleteLabel,\n {triggerElement});\n }).then(() => {\n const pendingPromise = new Pending('mod_lti/course_tools:delete');\n\n const request = {\n methodname: 'mod_lti_delete_course_tool_type',\n args: {tooltypeid: courseToolDelete.dataset.courseToolId}\n };\n return Ajax.call([request])[0]\n .then(addToast(getString('coursetooldeleted', 'mod_lti', courseToolDelete.dataset.courseToolName)))\n .then(() => {\n const tableRoot = triggerElement.closest(Selectors.main.region);\n return refreshTableContent(tableRoot);\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n\n const courseShowInActivityChooser = event.target.closest('[data-action=\"showinactivitychooser-toggle\"]');\n if (courseShowInActivityChooser) {\n const showInActivityChooserStateToggle = courseShowInActivityChooser.dataset.state === \"0\" ? 1 : 0;\n return toggleShowInActivityChooser(\n courseShowInActivityChooser.dataset.id,\n courseShowInActivityChooser.dataset.courseid,\n showInActivityChooserStateToggle,\n );\n }\n });\n};\n"],"names":["document","addEventListener","event","courseToolDelete","target","closest","preventDefault","deleteBodyStringId","dataset","courseToolUsage","requiredStrings","key","component","param","courseToolName","triggerElement","querySelector","then","_ref","modalTitle","modalBody","deleteLabel","Notification","deleteCancelPromise","pendingPromise","Pending","request","methodname","args","tooltypeid","courseToolId","Ajax","call","tableRoot","Selectors","main","region","resolve","catch","exception","courseShowInActivityChooser","showInActivityChooserStateToggle","state","id","courseid"],"mappings":"+/CAqCoB,KAChBA,SAASC,iBAAiB,SAASC,cAEzBC,iBAAmBD,MAAME,OAAOC,QAAQ,yCAC1CF,iBAAkB,CAClBD,MAAMI,uBAIAC,mBADQJ,iBAAiBK,QAAQC,gBACJ,EAAI,mCAAqC,0BACtEC,gBAAkB,CACpB,CAACC,IAAK,mBAAoBC,UAAW,UAAWC,MAAOV,iBAAiBK,QAAQM,gBAChF,CAACH,IAAKJ,mBAAoBK,UAAW,UAAWC,MAAOV,iBAAiBK,QAAQM,gBAChF,CAACH,IAAK,SAAUC,UAAW,OAAQC,MAAOV,iBAAiBK,QAAQM,gBACnE,CAACH,IAAK,oBAAqBC,UAAW,UAAWC,MAAOV,iBAAiBK,QAAQM,iBAG/EC,eAAiBZ,iBAAiBE,QAAQ,aAAaW,cAAc,wCAEhEN,iBAAiBO,MAAKC,WAAEC,WAAYC,UAAWC,yBAC/CC,sBAAaC,oBAChBJ,WACAC,UACAC,YACA,CAACN,eAAAA,oBACNE,MAAK,WACEO,eAAiB,IAAIC,iBAAQ,+BAE7BC,QAAU,CACZC,WAAY,kCACZC,KAAM,CAACC,WAAY1B,iBAAiBK,QAAQsB,sBAEzCC,cAAKC,KAAK,CAACN,UAAU,GACvBT,MAAK,eAAS,kBAAU,oBAAqB,UAAWd,iBAAiBK,QAAQM,kBACjFG,MAAK,WACIgB,UAAYlB,eAAeV,QAAQ6B,UAAUC,KAAKC,eACjD,gCAAoBH,cAE9BhB,KAAKO,eAAea,SACpBC,MAAMhB,sBAAaiB,cACzBD,OAAM,eAKPE,4BAA8BtC,MAAME,OAAOC,QAAQ,mDACrDmC,4BAA6B,OACvBC,iCAAiF,MAA9CD,4BAA4BhC,QAAQkC,MAAgB,EAAI,SAC1F,2CACHF,4BAA4BhC,QAAQmC,GACpCH,4BAA4BhC,QAAQoC,SACpCH"}amd/build/tool_card_controller.min.js.map000064400000074666152265520610014540 0ustar00{"version":3,"file":"tool_card_controller.min.js","sources":["../src/tool_card_controller.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls all of the behaviour and interaction with a tool type card. These are\n * listed on the LTI tool type management page.\n *\n * See template: mod_lti/tool_card\n *\n * @module mod_lti/tool_card_controller\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\n define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'core/modal',\n 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys',\n 'core/str'],\n function($, ajax, notification, templates, Modal, toolType, ltiEvents, KEYS, str) {\n\n var SELECTORS = {\n DELETE_BUTTON: '.delete',\n NAME_ELEMENT: '.name',\n DESCRIPTION_ELEMENT: '.description',\n CAPABILITIES_CONTAINER: '.capabilities-container',\n ACTIVATE_BUTTON: '.tool-card-footer a.activate',\n };\n\n // Timeout in seconds.\n var ANNOUNCEMENT_TIMEOUT = 2000;\n\n /**\n * Return the delete button element.\n *\n * @method getDeleteButton\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {JQuery} jQuery object\n */\n var getDeleteButton = function(element) {\n return element.find(SELECTORS.DELETE_BUTTON);\n };\n\n /**\n * Return the element representing the tool type name.\n *\n * @method getNameElement\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {JQuery} jQuery object\n */\n var getNameElement = function(element) {\n return element.find(SELECTORS.NAME_ELEMENT);\n };\n\n /**\n * Return the element representing the tool type description.\n *\n * @method getDescriptionElement\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {JQuery} jQuery object\n */\n var getDescriptionElement = function(element) {\n return element.find(SELECTORS.DESCRIPTION_ELEMENT);\n };\n\n /**\n * Return the activate button for the type.\n *\n * @method getActivateButton\n * @private\n * @param {Object} element jQuery object representing the tool card.\n * @return {Object} jQuery object\n */\n var getActivateButton = function(element) {\n return element.find(SELECTORS.ACTIVATE_BUTTON);\n };\n\n /**\n * Checks if the type card has an activate button.\n *\n * @method hasActivateButton\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Boolean} true if has active buton\n */\n var hasActivateButton = function(element) {\n return getActivateButton(element).length ? true : false;\n };\n\n /**\n * Return the element that contains the capabilities approval for\n * the user.\n *\n * @method getCapabilitiesContainer\n * @private\n * @param {Object} element jQuery object representing the tool card.\n * @return {Object} The element\n */\n var getCapabilitiesContainer = function(element) {\n return element.find(SELECTORS.CAPABILITIES_CONTAINER);\n };\n\n /**\n * Checks if the tool type has capabilities that need approval. If it\n * does then the container will be present.\n *\n * @method hasCapabilitiesContainer\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Boolean} true if has capbilities.\n */\n var hasCapabilitiesContainer = function(element) {\n return getCapabilitiesContainer(element).length ? true : false;\n };\n\n /**\n * Get the type id.\n *\n * @method getTypeId\n * @private\n * @param {Object} element jQuery object representing the tool card.\n * @return {String} Type ID\n */\n var getTypeId = function(element) {\n return element.attr('data-type-id');\n };\n\n /**\n * Stop any announcement currently visible on the card.\n *\n * @method clearAllAnnouncements\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var clearAllAnnouncements = function(element) {\n element.removeClass('announcement loading success fail capabilities');\n };\n\n /**\n * Show the loading announcement.\n *\n * @method startLoading\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var startLoading = function(element) {\n clearAllAnnouncements(element);\n element.addClass('announcement loading');\n };\n\n /**\n * Hide the loading announcement.\n *\n * @method stopLoading\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var stopLoading = function(element) {\n element.removeClass('announcement loading');\n };\n\n /**\n * Show the success announcement. The announcement is only\n * visible for 2 seconds.\n *\n * @method announceSuccess\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var announceSuccess = function(element) {\n var promise = $.Deferred();\n\n clearAllAnnouncements(element);\n element.addClass('announcement success');\n setTimeout(function() {\n element.removeClass('announcement success');\n promise.resolve();\n }, ANNOUNCEMENT_TIMEOUT);\n\n return promise;\n };\n\n /**\n * Show the failure announcement. The announcement is only\n * visible for 2 seconds.\n *\n * @method announceFailure\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var announceFailure = function(element) {\n var promise = $.Deferred();\n\n clearAllAnnouncements(element);\n element.addClass('announcement fail');\n setTimeout(function() {\n element.removeClass('announcement fail');\n promise.resolve();\n }, ANNOUNCEMENT_TIMEOUT);\n\n return promise;\n };\n\n /**\n * Delete the tool type from the Moodle server. Triggers a success\n * or failure announcement depending on the result.\n *\n * @method deleteType\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var deleteType = function(element) {\n var promise = $.Deferred();\n var typeId = getTypeId(element);\n startLoading(element);\n\n if (typeId === \"\") {\n return $.Deferred().resolve();\n }\n\n str.get_strings([\n {\n key: 'delete',\n component: 'mod_lti'\n },\n {\n key: 'delete_confirmation',\n component: 'mod_lti'\n },\n {\n key: 'delete',\n component: 'mod_lti'\n },\n {\n key: 'cancel',\n component: 'core'\n },\n ])\n .done(function(strs) {\n notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {\n toolType.delete(typeId)\n .done(function() {\n stopLoading(element);\n announceSuccess(element)\n .done(function() {\n element.remove();\n })\n .fail(notification.exception)\n .always(function() {\n // Always resolve because even if the announcement fails the type was deleted.\n promise.resolve();\n });\n })\n .fail(function(error) {\n announceFailure(element);\n promise.reject(error);\n });\n }, function() {\n stopLoading(element);\n promise.resolve();\n });\n })\n .fail(function(error) {\n stopLoading(element);\n notification.exception(error);\n promise.reject(error);\n });\n\n return promise;\n };\n\n /**\n * Save a given value in a data attribute on the element.\n *\n * @method setValueSnapshot\n * @private\n * @param {JQuery} element jQuery object representing the element.\n * @param {String} value to be saved.\n */\n var setValueSnapshot = function(element, value) {\n element.attr('data-val-snapshot', value);\n };\n\n /**\n * Return the saved value from the element.\n *\n * @method getValueSnapshot\n * @private\n * @param {JQuery} element jQuery object representing the element.\n * @return {String} the saved value.\n */\n var getValueSnapshot = function(element) {\n return element.attr('data-val-snapshot');\n };\n\n /**\n * Save the current value of the tool description.\n *\n * @method snapshotDescription\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var snapshotDescription = function(element) {\n var descriptionElement = getDescriptionElement(element);\n\n if (descriptionElement.hasClass('loading')) {\n return;\n }\n\n var description = descriptionElement.text().trim();\n setValueSnapshot(descriptionElement, description);\n };\n\n /**\n * Send a request to update the description value for this tool\n * in the Moodle server.\n *\n * @method updateDescription\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var updateDescription = function(element) {\n var typeId = getTypeId(element);\n\n // Return early if we don't have an id because it's\n // required to save the changes.\n if (typeId === \"\") {\n return $.Deferred().resolve();\n }\n\n var descriptionElement = getDescriptionElement(element);\n\n // Return early if we're already saving a value.\n if (descriptionElement.hasClass('loading')) {\n return $.Deferred().resolve();\n }\n\n var description = descriptionElement.text().trim();\n var snapshotVal = getValueSnapshot(descriptionElement);\n\n // If the value hasn't change then don't bother sending the\n // update request.\n if (snapshotVal == description) {\n return $.Deferred().resolve();\n }\n\n descriptionElement.addClass('loading');\n\n var promise = toolType.update({id: typeId, description: description});\n\n promise.done(function(type) {\n descriptionElement.removeClass('loading');\n // Make sure the text is updated with the description from the\n // server, just in case the update didn't work.\n descriptionElement.text(type.description);\n }).fail(notification.exception);\n\n // Probably need to handle failures better so that we can revert\n // the value in the input for the user.\n promise.fail(function() {\n descriptionElement.removeClass('loading');\n });\n\n return promise;\n };\n\n /**\n * Save the current value of the tool name.\n *\n * @method snapshotName\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var snapshotName = function(element) {\n var nameElement = getNameElement(element);\n\n if (nameElement.hasClass('loading')) {\n return;\n }\n\n var name = nameElement.text().trim();\n setValueSnapshot(nameElement, name);\n };\n\n /**\n * Send a request to update the name value for this tool\n * in the Moodle server.\n *\n * @method updateName\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var updateName = function(element) {\n var typeId = getTypeId(element);\n\n // Return if we don't have an id.\n if (typeId === \"\") {\n return $.Deferred().resolve();\n }\n\n var nameElement = getNameElement(element);\n\n // Return if we're already saving.\n if (nameElement.hasClass('loading')) {\n return $.Deferred().resolve();\n }\n\n var name = nameElement.text().trim();\n var snapshotVal = getValueSnapshot(nameElement);\n\n // If the value hasn't change then don't bother sending the\n // update request.\n if (snapshotVal == name) {\n return $.Deferred().resolve();\n }\n\n nameElement.addClass('loading');\n var promise = toolType.update({id: typeId, name: name});\n\n promise.done(function(type) {\n nameElement.removeClass('loading');\n // Make sure the text is updated with the name from the\n // server, just in case the update didn't work.\n nameElement.text(type.name);\n });\n\n // Probably need to handle failures better so that we can revert\n // the value in the input for the user.\n promise.fail(function() {\n nameElement.removeClass('loading');\n });\n\n return promise;\n };\n\n /**\n * Send a request to update the state for this tool to be configured (active)\n * in the Moodle server. A success or failure announcement is triggered depending\n * on the result.\n *\n * @method setStatusActive\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n * @return {Promise} jQuery Deferred object\n */\n var setStatusActive = function(element) {\n var id = getTypeId(element);\n\n // Return if we don't have an id.\n if (id === \"\") {\n return $.Deferred().resolve();\n }\n\n startLoading(element);\n\n var promise = toolType.update({\n id: id,\n state: toolType.constants.state.configured\n });\n\n promise.then(function(toolTypeData) {\n stopLoading(element);\n announceSuccess(element);\n return toolTypeData;\n }).then(function(toolTypeData) {\n return templates.render('mod_lti/tool_card', toolTypeData);\n }).then(function(html, js) {\n templates.replaceNode(element, html, js);\n return;\n }).catch(function() {\n stopLoading(element);\n announceFailure(element);\n });\n\n return promise;\n };\n\n /**\n * Show the capabilities approval screen to show which groups of data this\n * type requires access to in Moodle (if any).\n *\n * @method displayCapabilitiesApproval\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var displayCapabilitiesApproval = function(element) {\n element.addClass('announcement capabilities');\n };\n\n /**\n * Hide the capabilities approval screen.\n *\n * @method hideCapabilitiesApproval\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var hideCapabilitiesApproval = function(element) {\n element.removeClass('announcement capabilities');\n };\n\n /**\n * The user wishes to activate this tool so show them the capabilities that\n * they need to agree to or if there are none then set the tool type's state\n * to active.\n *\n * @method activateToolType\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var activateToolType = function(element) {\n if (hasCapabilitiesContainer(element)) {\n displayCapabilitiesApproval(element);\n } else {\n setStatusActive(element);\n }\n };\n\n /**\n * Sets up the listeners for user interaction on this tool type card.\n *\n * @method registerEventListeners\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var registerEventListeners = function(element) {\n var deleteButton = getDeleteButton(element);\n deleteButton.click(function(e) {\n e.preventDefault();\n deleteType(element);\n });\n deleteButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n deleteButton.click();\n }\n }\n });\n\n var descriptionElement = getDescriptionElement(element);\n descriptionElement.focus(function(e) {\n e.preventDefault();\n // Save a copy of the current value for the description so that\n // we can check if the user has changed it before sending a request to\n // the server.\n snapshotDescription(element);\n });\n descriptionElement.blur(function(e) {\n e.preventDefault();\n updateDescription(element);\n });\n descriptionElement.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER) {\n e.preventDefault();\n descriptionElement.blur();\n }\n }\n });\n\n var nameElement = getNameElement(element);\n nameElement.focus(function(e) {\n e.preventDefault();\n // Save a copy of the current value for the name so that\n // we can check if the user has changed it before sending a request to\n // the server.\n snapshotName(element);\n });\n nameElement.blur(function(e) {\n e.preventDefault();\n updateName(element);\n });\n nameElement.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER) {\n e.preventDefault();\n nameElement.blur();\n }\n }\n });\n\n // Only pending tool type cards have an activate button.\n if (hasActivateButton(element)) {\n var activateButton = getActivateButton(element);\n activateButton.click(function(e) {\n e.preventDefault();\n activateToolType(element);\n });\n activateButton.keypress(function(e) {\n if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {\n if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {\n e.preventDefault();\n activateButton.click();\n }\n }\n });\n }\n\n if (hasCapabilitiesContainer(element)) {\n var capabilitiesContainer = getCapabilitiesContainer(element);\n\n capabilitiesContainer.on(ltiEvents.CAPABILITIES_AGREE, function() {\n setStatusActive(element);\n });\n\n capabilitiesContainer.on(ltiEvents.CAPABILITIES_DECLINE, function() {\n hideCapabilitiesApproval(element);\n });\n }\n };\n\n /**\n * Sets up the templates for the tool configuration modal on this tool type card.\n *\n * @method registerModal\n * @private\n * @param {JQuery} element jQuery object representing the tool card.\n */\n var registerModal = function(element) {\n const configurationLink = element.find('#' + element.data('uniqid') + '-' + element.data('deploymentid'));\n if (!configurationLink.length) {\n return;\n }\n const trigger = configurationLink.get(0);\n trigger.addEventListener('click', (e) => {\n e.preventDefault();\n var context = {\n 'uniqid': element.data('uniqid'),\n 'platformid': element.data('platformid'),\n 'clientid': element.data('clientid'),\n 'deploymentid': element.data('deploymentid'),\n 'urls': {\n 'publickeyset': element.data('publickeyseturl'),\n 'accesstoken': element.data('accesstokenurl'),\n 'authrequest': element.data('authrequesturl')\n }\n };\n var bodyPromise = templates.render('mod_lti/tool_config_modal_body', context);\n var mailTo = 'mailto:?subject=' + encodeURIComponent(element.data('mailtosubject')) +\n '&body=' + encodeURIComponent(element.data('platformidstr')) + ':%20' +\n encodeURIComponent(element.data('platformid')) + '%0D%0A' +\n encodeURIComponent(element.data('clientidstr')) + ':%20' +\n encodeURIComponent(element.data('clientid')) + '%0D%0A' +\n encodeURIComponent(element.data('deploymentidstr')) + ':%20' +\n encodeURIComponent(element.data('deploymentid')) + '%0D%0A' +\n encodeURIComponent(element.data('publickeyseturlstr')) + ':%20' +\n encodeURIComponent(element.data('publickeyseturl')) + '%0D%0A' +\n encodeURIComponent(element.data('accesstokenurlstr')) + ':%20' +\n encodeURIComponent(element.data('accesstokenurl')) + '%0D%0A' +\n encodeURIComponent(element.data('authrequesturlstr')) + ':%20' +\n encodeURIComponent(element.data('authrequesturl')) + '%0D%0A';\n context = {\n 'mailto': mailTo\n };\n var footerPromise = templates.render('mod_lti/tool_config_modal_footer', context);\n Modal.create({\n large: true,\n title: element.data('modaltitle'),\n body: bodyPromise,\n footer: footerPromise,\n show: true\n });\n });\n };\n\n return /** @alias module:mod_lti/tool_card_controller */ {\n\n /**\n * Initialise this module.\n *\n * @param {JQuery} element jQuery object representing the tool card.\n */\n init: function(element) {\n registerEventListeners(element);\n registerModal(element);\n }\n };\n});\n"],"names":["define","$","ajax","notification","templates","Modal","toolType","ltiEvents","KEYS","str","SELECTORS","getNameElement","element","find","getDescriptionElement","getActivateButton","getCapabilitiesContainer","hasCapabilitiesContainer","length","getTypeId","attr","clearAllAnnouncements","removeClass","startLoading","addClass","stopLoading","announceSuccess","promise","Deferred","setTimeout","resolve","announceFailure","setValueSnapshot","value","getValueSnapshot","setStatusActive","id","update","state","constants","configured","then","toolTypeData","render","html","js","replaceNode","catch","registerEventListeners","deleteButton","getDeleteButton","click","e","preventDefault","typeId","get_strings","key","component","done","strs","confirm","delete","remove","fail","exception","always","error","reject","deleteType","keypress","metaKey","shiftKey","altKey","ctrlKey","keyCode","ENTER","SPACE","descriptionElement","focus","hasClass","description","text","trim","snapshotDescription","blur","type","updateDescription","nameElement","name","snapshotName","updateName","hasActivateButton","activateButton","displayCapabilitiesApproval","activateToolType","capabilitiesContainer","on","CAPABILITIES_AGREE","CAPABILITIES_DECLINE","hideCapabilitiesApproval","init","configurationLink","data","get","addEventListener","context","bodyPromise","encodeURIComponent","footerPromise","create","large","title","body","footer","show","registerModal"],"mappings":";;;;;;;;;;;AA0BCA,sCAAO,CAAC,SAAU,YAAa,oBAAqB,iBAAkB,aAC/D,oBAAqB,iBAAkB,eACvC,aACA,SAASC,EAAGC,KAAMC,aAAcC,UAAWC,MAAOC,SAAUC,UAAWC,KAAMC,SAE7EC,wBACe,UADfA,uBAEc,QAFdA,8BAGqB,eAHrBA,iCAIwB,0BAJxBA,0BAKiB,+BA0BjBC,eAAiB,SAASC,gBACnBA,QAAQC,KAAKH,yBAWpBI,sBAAwB,SAASF,gBAC1BA,QAAQC,KAAKH,gCAWpBK,kBAAoB,SAASH,gBACtBA,QAAQC,KAAKH,4BAwBpBM,yBAA2B,SAASJ,gBAC7BA,QAAQC,KAAKH,mCAYpBO,yBAA2B,SAASL,iBAC7BI,yBAAyBJ,SAASM,QAWzCC,UAAY,SAASP,gBACdA,QAAQQ,KAAK,iBAUpBC,sBAAwB,SAAST,SACjCA,QAAQU,YAAY,mDAUpBC,aAAe,SAASX,SACxBS,sBAAsBT,SACtBA,QAAQY,SAAS,yBAUjBC,YAAc,SAASb,SACvBA,QAAQU,YAAY,yBAYpBI,gBAAkB,SAASd,aACvBe,QAAU1B,EAAE2B,kBAEhBP,sBAAsBT,SACtBA,QAAQY,SAAS,wBACjBK,YAAW,WACPjB,QAAQU,YAAY,wBACpBK,QAAQG,YAtJW,KAyJhBH,SAYPI,gBAAkB,SAASnB,aACvBe,QAAU1B,EAAE2B,kBAEhBP,sBAAsBT,SACtBA,QAAQY,SAAS,qBACjBK,YAAW,WACPjB,QAAQU,YAAY,qBACpBK,QAAQG,YA5KW,KA+KhBH,SAgFPK,iBAAmB,SAASpB,QAASqB,OACrCrB,QAAQQ,KAAK,oBAAqBa,QAWlCC,iBAAmB,SAAStB,gBACrBA,QAAQQ,KAAK,sBA2JpBe,gBAAkB,SAASvB,aACvBwB,GAAKjB,UAAUP,YAGR,KAAPwB,UACOnC,EAAE2B,WAAWE,UAGxBP,aAAaX,aAETe,QAAUrB,SAAS+B,OAAO,CAC1BD,GAAIA,GACJE,MAAOhC,SAASiC,UAAUD,MAAME,oBAGpCb,QAAQc,MAAK,SAASC,qBAClBjB,YAAYb,SACZc,gBAAgBd,SACT8B,gBACRD,MAAK,SAASC,qBACNtC,UAAUuC,OAAO,oBAAqBD,iBAC9CD,MAAK,SAASG,KAAMC,IACnBzC,UAAU0C,YAAYlC,QAASgC,KAAMC,OAEtCE,OAAM,WACLtB,YAAYb,SACZmB,gBAAgBnB,YAGbe,SAkDPqB,uBAAyB,SAASpC,aAC9BqC,aA7ec,SAASrC,gBACpBA,QAAQC,KAAKH,yBA4eDwC,CAAgBtC,SACnCqC,aAAaE,OAAM,SAASC,GACxBA,EAAEC,iBA9TO,SAASzC,aAClBe,QAAU1B,EAAE2B,WACZ0B,OAASnC,UAAUP,SACvBW,aAAaX,SAEE,KAAX0C,OACOrD,EAAE2B,WAAWE,UAGxBrB,IAAI8C,YAAY,CACR,CACIC,IAAK,SACLC,UAAW,WAEf,CACID,IAAK,sBACLC,UAAW,WAEf,CACID,IAAK,SACLC,UAAW,WAEf,CACID,IAAK,SACLC,UAAW,UAGlBC,MAAK,SAASC,MACPxD,aAAayD,QAAQD,KAAK,GAAIA,KAAK,GAAIA,KAAK,GAAIA,KAAK,IAAI,WACjDrD,SAASuD,OAAOP,QACXI,MAAK,WACEjC,YAAYb,SACZc,gBAAgBd,SACX8C,MAAK,WACE9C,QAAQkD,YAEfC,KAAK5D,aAAa6D,WAClBC,QAAO,WAEAtC,QAAQG,gBAG3BiC,MAAK,SAASG,OACPnC,gBAAgBnB,SAChBe,QAAQwC,OAAOD,aAE5B,WACKzC,YAAYb,SACZe,QAAQG,gBAG3BiC,MAAK,SAASG,OACPzC,YAAYb,SACZT,aAAa6D,UAAUE,OACvBvC,QAAQwC,OAAOD,UAyQvBE,CAAWxD,YAEfqC,aAAaoB,UAAS,SAASjB,GACtBA,EAAEkB,SAAYlB,EAAEmB,UAAanB,EAAEoB,QAAWpB,EAAEqB,SACzCrB,EAAEsB,SAAWlE,KAAKmE,OAASvB,EAAEsB,SAAWlE,KAAKoE,QAC7CxB,EAAEC,iBACFJ,aAAaE,gBAKrB0B,mBAAqB/D,sBAAsBF,SAC/CiE,mBAAmBC,OAAM,SAAS1B,GAC9BA,EAAEC,iBAjPgB,SAASzC,aAC3BiE,mBAAqB/D,sBAAsBF,aAE3CiE,mBAAmBE,SAAS,gBAI5BC,YAAcH,mBAAmBI,OAAOC,OAC5ClD,iBAAiB6C,mBAAoBG,cA6OjCG,CAAoBvE,YAExBiE,mBAAmBO,MAAK,SAAShC,GAC7BA,EAAEC,iBApOc,SAASzC,aACzB0C,OAASnC,UAAUP,YAIR,KAAX0C,cACOrD,EAAE2B,WAAWE,cAGpB+C,mBAAqB/D,sBAAsBF,YAG3CiE,mBAAmBE,SAAS,kBACrB9E,EAAE2B,WAAWE,cAGpBkD,YAAcH,mBAAmBI,OAAOC,UAC1BhD,iBAAiB2C,qBAIhBG,mBACR/E,EAAE2B,WAAWE,UAGxB+C,mBAAmBrD,SAAS,eAExBG,QAAUrB,SAAS+B,OAAO,CAACD,GAAIkB,OAAQ0B,YAAaA,cAExDrD,QAAQ+B,MAAK,SAAS2B,MAClBR,mBAAmBvD,YAAY,WAG/BuD,mBAAmBI,KAAKI,KAAKL,gBAC9BjB,KAAK5D,aAAa6D,WAIrBrC,QAAQoC,MAAK,WACXc,mBAAmBvD,YAAY,cA8L7BgE,CAAkB1E,YAEtBiE,mBAAmBR,UAAS,SAASjB,GAC5BA,EAAEkB,SAAYlB,EAAEmB,UAAanB,EAAEoB,QAAWpB,EAAEqB,SACzCrB,EAAEsB,SAAWlE,KAAKmE,QAClBvB,EAAEC,iBACFwB,mBAAmBO,eAK3BG,YAAc5E,eAAeC,YACjC2E,YAAYT,OAAM,SAAS1B,GACvBA,EAAEC,iBA9LS,SAASzC,aACpB2E,YAAc5E,eAAeC,aAE7B2E,YAAYR,SAAS,gBAIrBS,KAAOD,YAAYN,OAAOC,OAC9BlD,iBAAiBuD,YAAaC,OA0L1BC,CAAa7E,YAEjB2E,YAAYH,MAAK,SAAShC,GACtBA,EAAEC,iBAjLO,SAASzC,aAClB0C,OAASnC,UAAUP,YAGR,KAAX0C,cACOrD,EAAE2B,WAAWE,cAGpByD,YAAc5E,eAAeC,YAG7B2E,YAAYR,SAAS,kBACd9E,EAAE2B,WAAWE,cAGpB0D,KAAOD,YAAYN,OAAOC,UACZhD,iBAAiBqD,cAIhBC,YACRvF,EAAE2B,WAAWE,UAGxByD,YAAY/D,SAAS,eACjBG,QAAUrB,SAAS+B,OAAO,CAACD,GAAIkB,OAAQkC,KAAMA,OAEjD7D,QAAQ+B,MAAK,SAAS2B,MAClBE,YAAYjE,YAAY,WAGxBiE,YAAYN,KAAKI,KAAKG,SAK1B7D,QAAQoC,MAAK,WACXwB,YAAYjE,YAAY,cA6ItBoE,CAAW9E,YAEf2E,YAAYlB,UAAS,SAASjB,GACrBA,EAAEkB,SAAYlB,EAAEmB,UAAanB,EAAEoB,QAAWpB,EAAEqB,SACzCrB,EAAEsB,SAAWlE,KAAKmE,QAClBvB,EAAEC,iBACFkC,YAAYH,WAhfJ,SAASxE,iBACtBG,kBAAkBH,SAASM,OAqf9ByE,CAAkB/E,SAAU,KACxBgF,eAAiB7E,kBAAkBH,SACvCgF,eAAezC,OAAM,SAASC,GAC1BA,EAAEC,iBA5ES,SAASzC,SACxBK,yBAAyBL,SAzBC,SAASA,SACvCA,QAAQY,SAAS,6BAyBbqE,CAA4BjF,SAE5BuB,gBAAgBvB,SAyEZkF,CAAiBlF,YAErBgF,eAAevB,UAAS,SAASjB,GACxBA,EAAEkB,SAAYlB,EAAEmB,UAAanB,EAAEoB,QAAWpB,EAAEqB,SACzCrB,EAAEsB,SAAWlE,KAAKmE,OAASvB,EAAEsB,SAAWlE,KAAKoE,QAC7CxB,EAAEC,iBACFuC,eAAezC,eAM3BlC,yBAAyBL,SAAU,KAC/BmF,sBAAwB/E,yBAAyBJ,SAErDmF,sBAAsBC,GAAGzF,UAAU0F,oBAAoB,WACnD9D,gBAAgBvB,YAGpBmF,sBAAsBC,GAAGzF,UAAU2F,sBAAsB,YA7GlC,SAAStF,SACpCA,QAAQU,YAAY,6BA6GZ6E,CAAyBvF,oBA2DoB,CAOrDwF,KAAM,SAASxF,SACXoC,uBAAuBpC,SAvDX,SAASA,eACnByF,kBAAoBzF,QAAQC,KAAK,IAAMD,QAAQ0F,KAAK,UAAY,IAAM1F,QAAQ0F,KAAK,qBACpFD,kBAAkBnF,cAGPmF,kBAAkBE,IAAI,GAC9BC,iBAAiB,SAAUpD,IAC/BA,EAAEC,qBACEoD,QAAU,QACA7F,QAAQ0F,KAAK,qBACT1F,QAAQ0F,KAAK,uBACf1F,QAAQ0F,KAAK,yBACT1F,QAAQ0F,KAAK,qBACrB,cACY1F,QAAQ0F,KAAK,+BACd1F,QAAQ0F,KAAK,8BACb1F,QAAQ0F,KAAK,oBAGhCI,YAActG,UAAUuC,OAAO,iCAAkC8D,SAcrEA,QAAU,QAbG,mBAAqBE,mBAAmB/F,QAAQ0F,KAAK,kBAC9D,SAAWK,mBAAmB/F,QAAQ0F,KAAK,kBAAoB,OAC/DK,mBAAmB/F,QAAQ0F,KAAK,eAAiB,SACjDK,mBAAmB/F,QAAQ0F,KAAK,gBAAkB,OAClDK,mBAAmB/F,QAAQ0F,KAAK,aAAe,SAC/CK,mBAAmB/F,QAAQ0F,KAAK,oBAAsB,OACtDK,mBAAmB/F,QAAQ0F,KAAK,iBAAmB,SACnDK,mBAAmB/F,QAAQ0F,KAAK,uBAAyB,OACzDK,mBAAmB/F,QAAQ0F,KAAK,oBAAsB,SACtDK,mBAAmB/F,QAAQ0F,KAAK,sBAAwB,OACxDK,mBAAmB/F,QAAQ0F,KAAK,mBAAqB,SACrDK,mBAAmB/F,QAAQ0F,KAAK,sBAAwB,OACxDK,mBAAmB/F,QAAQ0F,KAAK,mBAAqB,cAIrDM,cAAgBxG,UAAUuC,OAAO,mCAAoC8D,SACzEpG,MAAMwG,OAAO,CACTC,OAAO,EACPC,MAAOnG,QAAQ0F,KAAK,cACpBU,KAAMN,YACNO,OAAQL,cACRM,MAAM,OAcVC,CAAcvG"}amd/build/keys.min.js.map000064400000002416152265520610011262 0ustar00{"version":3,"file":"keys.min.js","sources":["../src/keys.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A list of keys and their keycodes that are used by the LTI modules.\n *\n * @module mod_lti/keys\n * @class keys\n * @copyright 2015 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine([], function() {\n return /** @alias module:mod_lti/keys */ {\n ENTER: 13,\n SPACE: 32\n };\n});\n"],"names":["define","ENTER","SPACE"],"mappings":";;;;;;;;;AAwBAA,sBAAO,IAAI,iBACkC,CACrCC,MAAO,GACPC,MAAO"}amd/build/coursecategory.min.js.map000064400000014230152265520610013342 0ustar00{"version":3,"file":"coursecategory.min.js","sources":["../src/coursecategory.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Select course categories for LTI tool.\n *\n * @module mod_lti/coursecategory\n * @copyright 2023 Jackson D'souza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.3\n */\n\ndefine([], function() {\n\n document.addEventListener('click', event => {\n const checkedbox = event.target.closest(\".lticoursecategories\");\n\n if (checkedbox) {\n // Get checkbox status.\n const checkboxstatus = checkedbox.checked;\n\n // Check / Uncheck all child category checkboxes based on selected checkbox status.\n const categorycontainer = document.querySelector('#collapse' + checkedbox.value);\n if (categorycontainer) {\n const categorycontainercheckbox = categorycontainer.querySelectorAll('input[type=\"checkbox\"]');\n for (let i = 0; i < categorycontainercheckbox.length; i++) {\n categorycontainercheckbox[i].checked = checkboxstatus;\n }\n }\n\n const lticategorytree = document.querySelector('.modltitree');\n const ltitreecheckbox = lticategorytree.querySelectorAll('input[type=\"checkbox\"]');\n let listvalue = '';\n for (let i = 0; i < ltitreecheckbox.length; i++) {\n if (ltitreecheckbox[i].checked) {\n if (listvalue.length == 0) {\n listvalue = ltitreecheckbox[i].value;\n } else {\n listvalue = listvalue + ',' + ltitreecheckbox[i].value;\n }\n }\n }\n document.querySelector('input[name=\"lti_coursecategories\"]').value = listvalue;\n }\n });\n\n /**\n * Get parent elements with class = accordion.\n *\n * @method getParents\n * @private\n * @param {string} elem DOM element.\n * @return {array} Parent elements.\n */\n function getParents(elem) {\n // Set up a parent array\n const parents = [];\n\n // Push each parent element to the array\n for (; elem && elem !== document; elem = elem.parentNode) {\n if (elem.classList.contains('accordion-group')) {\n parents.push(elem);\n }\n }\n\n // Return our parent array\n return parents;\n }\n\n return /** @alias module:mod_lti/coursecategory */ {\n\n /**\n * Initialise this module.\n * Loop through checkbox form elements starting with #cat-{N} and set it to checked\n * if {N} is found in the Selected category(s) list. Show / Hide the parent UL element.\n *\n * @param {string} selectedcategories Selected category(s).\n */\n init: function(selectedcategories) {\n if (selectedcategories.length) {\n const separator = \",\";\n const values = selectedcategories.split(separator);\n\n for (let i = 0; i < values.length; i++) {\n const categoryid = document.getElementById(\"cat-\" + values[i]);\n if (categoryid.value !== 0) {\n categoryid.checked = true;\n }\n const parents = getParents(categoryid);\n parents.forEach(function(element) {\n const elem = element.querySelector('a.accordion-toggle');\n const elembody = element.querySelector('.accordion-body');\n\n if (elem && elem.classList.contains('collapsed')) {\n elem.classList.remove('collapsed');\n }\n if (elembody) {\n elembody.classList.remove('collapse');\n elembody.classList.add('show');\n }\n });\n }\n }\n }\n };\n});"],"names":["define","getParents","elem","parents","document","parentNode","classList","contains","push","addEventListener","event","checkedbox","target","closest","checkboxstatus","checked","categorycontainer","querySelector","value","categorycontainercheckbox","querySelectorAll","i","length","ltitreecheckbox","listvalue","init","selectedcategories","separator","values","split","categoryid","getElementById","forEach","element","elembody","remove","add"],"mappings":";;;;;;;;AAwBAA,gCAAO,IAAI,oBA0CEC,WAAWC,YAEVC,QAAU,QAGTD,MAAQA,OAASE,SAAUF,KAAOA,KAAKG,WACtCH,KAAKI,UAAUC,SAAS,oBACxBJ,QAAQK,KAAKN,aAKdC,eApDXC,SAASK,iBAAiB,SAASC,cACzBC,WAAaD,MAAME,OAAOC,QAAQ,2BAEpCF,WAAY,OAENG,eAAiBH,WAAWI,QAG5BC,kBAAoBZ,SAASa,cAAc,YAAcN,WAAWO,UACtEF,kBAAmB,OACbG,0BAA4BH,kBAAkBI,iBAAiB,8BAChE,IAAIC,EAAI,EAAGA,EAAIF,0BAA0BG,OAAQD,IAClDF,0BAA0BE,GAAGN,QAAUD,qBAKzCS,gBADkBnB,SAASa,cAAc,eACPG,iBAAiB,8BACrDI,UAAY,OACX,IAAIH,EAAI,EAAGA,EAAIE,gBAAgBD,OAAQD,IACpCE,gBAAgBF,GAAGN,UAEfS,UADoB,GAApBA,UAAUF,OACEC,gBAAgBF,GAAGH,MAEnBM,UAAY,IAAMD,gBAAgBF,GAAGH,OAI7Dd,SAASa,cAAc,sCAAsCC,MAAQM,cA2B1B,CAS/CC,KAAM,SAASC,uBACPA,mBAAmBJ,OAAQ,OACrBK,UAAY,IACZC,OAASF,mBAAmBG,MAAMF,eAEnC,IAAIN,EAAI,EAAGA,EAAIO,OAAON,OAAQD,IAAK,OAC9BS,WAAa1B,SAAS2B,eAAe,OAASH,OAAOP,IAClC,IAArBS,WAAWZ,QACXY,WAAWf,SAAU,GAETd,WAAW6B,YACnBE,SAAQ,SAASC,eACf/B,KAAO+B,QAAQhB,cAAc,sBAC7BiB,SAAWD,QAAQhB,cAAc,mBAEnCf,MAAQA,KAAKI,UAAUC,SAAS,cAChCL,KAAKI,UAAU6B,OAAO,aAEtBD,WACAA,SAAS5B,UAAU6B,OAAO,YAC1BD,SAAS5B,UAAU8B,IAAI"}amd/build/tool_configure_controller.min.js000064400000016071152265520610015016 0ustar00/** * Standard Ajax wrapper for Moodle. It calls the central Ajax script, * which can call any existing webservice using the current session. * In addition, it can batch multiple requests and return multiple responses. * * @module mod_lti/tool_configure_controller * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("mod_lti/tool_configure_controller",["jquery","core/ajax","core/paged_content_factory","core/notification","core/templates","mod_lti/events","mod_lti/keys","mod_lti/tool_types_and_proxies","mod_lti/tool_type","mod_lti/tool_proxy","core/str","core/config"],(function($,ajax,pagedContentFactory,notification,templates,ltiEvents,KEYS,toolTypesAndProxies,toolType,toolProxy,str,config){var SELECTORS_EXTERNAL_REGISTRATION_CONTAINER="#external-registration-container",SELECTORS_EXTERNAL_REGISTRATION_PAGE_CONTAINER="#external-registration-page-container",SELECTORS_EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER="#external-registration-template-container",SELECTORS_CARTRIDGE_REGISTRATION_CONTAINER="#cartridge-registration-container",SELECTORS_CARTRIDGE_REGISTRATION_FORM="#cartridge-registration-form",SELECTORS_TOOL_CARD_CONTAINER="#tool-card-container",SELECTORS_TOOL_LIST_CONTAINER="#tool-list-container",SELECTORS_TOOL_CREATE_BUTTON="#tool-create-button",SELECTORS_TOOL_CREATE_LTILEGACY_BUTTON="#tool-createltilegacy-button",SELECTORS_REGISTRATION_CHOICE_CONTAINER="#registration-choice-container",SELECTORS_TOOL_URL="#tool-url",getToolListContainer=function(){return $(SELECTORS_TOOL_LIST_CONTAINER)};var getExternalRegistrationContainer=function(){return $(SELECTORS_EXTERNAL_REGISTRATION_CONTAINER)},getCartridgeRegistrationContainer=function(){return $(SELECTORS_CARTRIDGE_REGISTRATION_CONTAINER)},getRegistrationChoiceContainer=function(){return $(SELECTORS_REGISTRATION_CHOICE_CONTAINER)},closeLTIAdvRegistration=function(e){e.data&&"org.imsglobal.lti.close"===e.data.subject&&($(SELECTORS_EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty(),hideExternalRegistration(),showRegistrationChoices(),showToolList(),showRegistrationChoices(),reloadToolList())},getToolURL=function(){return $(SELECTORS_TOOL_URL).val()},hideExternalRegistration=function(){getExternalRegistrationContainer().addClass("hidden")},hideCartridgeRegistration=function(){getCartridgeRegistrationContainer().addClass("hidden")},hideRegistrationChoices=function(){getRegistrationChoiceContainer().addClass("hidden")},showExternalRegistration=function(){hideCartridgeRegistration(),hideRegistrationChoices(),getExternalRegistrationContainer().removeClass("hidden"),screenReaderAnnounce(getExternalRegistrationContainer())},showRegistrationChoices=function(){hideExternalRegistration(),hideCartridgeRegistration(),getRegistrationChoiceContainer().removeClass("hidden"),screenReaderAnnounce(getRegistrationChoiceContainer())},screenReaderAnnounce=function(element){element.children().detach().appendTo(element)},hideToolList=function(){getToolListContainer().addClass("hidden")},showToolList=function(){getToolListContainer().removeClass("hidden")},startLoading=function(element){element.addClass("loading")},stopLoading=function(element){element.removeClass("loading")},reloadToolList=function(){M.util.js_pending("reloadToolList");const cardContainer=$(SELECTORS_TOOL_CARD_CONTAINER),listContainer=getToolListContainer();fetchToolCount().done((function(data){pagedContentFactory.createWithTotalAndLimit(data.count,60,(function(pagesData){return pagesData.map((function(pageData){return fetchToolData(pageData.limit,pageData.offset).then((function(data){return renderToolData(data)}))}))}),{showFirstLast:!0}).done((function(html,js){templates.replaceNodeContents(cardContainer,html,js)})).always((function(){stopLoading(listContainer),M.util.js_complete("reloadToolList")}))})),startLoading(listContainer)};const fetchToolCount=function(){return toolTypesAndProxies.count({orphanedonly:!0}).done((function(data){return data})).catch((function(error){return notification.exception(error),{count:0}}))},fetchToolData=function(limit,offset){const args={orphanedonly:!0};return null===limit||Number.isNaN(limit)||(args.limit=limit),null===offset||Number.isNaN(offset)||(args.offset=offset),toolTypesAndProxies.query(args).done((function(data){return data})).catch((function(error){return notification.exception(error),{types:[],proxies:[],limit:limit,offset:offset}}))},renderToolData=function(data){const context={tools:data.types,proxies:data.proxies};return templates.render("mod_lti/tool_list",context).done((function(html,js){return{html:html,js:js}}))};var addLTIAdvTool=function(){var url=getToolURL().trim();url&&($(SELECTORS_TOOL_URL).val(""),hideToolList(),function(url){$(SELECTORS_EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass("hidden"),$(SELECTORS_EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).append($("")),showExternalRegistration(),window.addEventListener("message",closeLTIAdvRegistration,!1)}(url))},registerEventListeners=function(){$(document).on(ltiEvents.NEW_TOOL_TYPE,(function(){reloadToolList()})),$(document).on(ltiEvents.START_EXTERNAL_REGISTRATION,(function(){showExternalRegistration(),$(SELECTORS_TOOL_URL).val(""),hideToolList()})),$(document).on(ltiEvents.STOP_EXTERNAL_REGISTRATION,(function(){showToolList(),showRegistrationChoices()})),$(document).on(ltiEvents.START_CARTRIDGE_REGISTRATION,(function(event,url){!function(url){hideExternalRegistration(),hideRegistrationChoices();var container=getCartridgeRegistrationContainer();container.find("input").val(""),container.removeClass("hidden"),container.find(SELECTORS_CARTRIDGE_REGISTRATION_FORM).attr("data-cartridge-url",url),screenReaderAnnounce(container)}(url)})),$(document).on(ltiEvents.STOP_CARTRIDGE_REGISTRATION,(function(){getCartridgeRegistrationContainer().find(SELECTORS_CARTRIDGE_REGISTRATION_FORM).removeAttr("data-cartridge-url"),showRegistrationChoices()})),$(document).on(ltiEvents.REGISTRATION_FEEDBACK,(function(event,data){!function(data){var type=data.error?"error":"success";notification.addNotification({message:data.message,type:type})}(data)})),$(SELECTORS_TOOL_CREATE_LTILEGACY_BUTTON).click((function(e){e.preventDefault(),function(){var url=getToolURL().trim();if(""===url)return $.Deferred().resolve();var toolButton=$(SELECTORS_TOOL_CREATE_LTILEGACY_BUTTON);startLoading(toolButton);var promise=toolType.isCartridge(url);promise.always((function(){stopLoading(toolButton)})),promise.done((function(result){result.iscartridge?($(SELECTORS_TOOL_URL).val(""),$(document).trigger(ltiEvents.START_CARTRIDGE_REGISTRATION,url)):$(document).trigger(ltiEvents.START_EXTERNAL_REGISTRATION,{url:url})})),promise.fail((function(){str.get_string("errorbadurl","mod_lti").done((function(s){$(document).trigger(ltiEvents.REGISTRATION_FEEDBACK,{message:s,error:!0})})).fail(notification.exception)}))}()})),$(SELECTORS_TOOL_CREATE_BUTTON).click((function(e){e.preventDefault(),addLTIAdvTool()}))};return{init:function(){registerEventListeners(),reloadToolList()}}})); //# sourceMappingURL=tool_configure_controller.min.js.mapamd/build/contentitem_return.min.js000064400000001062152265520610013457 0ustar00/** * Processes the result of LTI tool creation from a Content-Item message type. * * @module mod_lti/contentitem_return * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.2 */ define("mod_lti/contentitem_return",["jquery"],(function($){return{init:function(returnData){$(window).ready((function(){window!=top?parent.processContentItemReturnData(returnData):window.processContentItemReturnData(returnData)}))}}})); //# sourceMappingURL=contentitem_return.min.js.mapamd/build/contentitem.min.js.map000064400000036555152265520610012653 0ustar00{"version":3,"file":"contentitem.min.js","sources":["../src/contentitem.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an\n * LTI tool provider that supports Content-Item type message.\n *\n * See template: mod_lti/contentitem\n *\n * @module mod_lti/contentitem\n * @copyright 2016 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine(\n [\n 'jquery',\n 'core/notification',\n 'core/str',\n 'core/templates',\n 'mod_lti/form-field',\n 'core/modal',\n 'core/modal_events'\n ],\n function($, notification, str, templates, FormField, Modal, ModalEvents) {\n var dialogue;\n var doneCallback;\n var contentItem = {\n /**\n * Init function.\n *\n * @param {string} url The URL for the content item selection.\n * @param {string} toolUrl The URL of the LTI tool.\n * @param {object} postData The data to be sent for the content item selection request.\n * @param {Function} cb The callback to run once the content item has been processed.\n */\n init: function(url, toolUrl, postData, cb) {\n doneCallback = cb;\n var context = {\n url: url,\n toolUrl: toolUrl,\n postData: postData\n };\n var bodyPromise = templates.render('mod_lti/contentitem', context);\n\n if (dialogue) {\n // Set dialogue body.\n dialogue.setBody(bodyPromise);\n // Display the dialogue.\n dialogue.show();\n return;\n }\n\n str.get_string('selectcontent', 'lti').then(function(title) {\n return Modal.create({\n title: title,\n body: bodyPromise,\n large: true,\n show: true,\n });\n }).then(function(modal) {\n dialogue = modal;\n // On hide handler.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Empty modal contents when it's hidden.\n modal.setBody('');\n\n // Fetch notifications.\n notification.fetchNotifications();\n });\n return;\n }).catch(notification.exception);\n }\n };\n\n /**\n * Array of form fields for LTI tool configuration.\n */\n var ltiFormFields = [\n new FormField('name', FormField.TYPES.TEXT, false, ''),\n new FormField('introeditor', FormField.TYPES.EDITOR, false, ''),\n new FormField('toolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('securetoolurl', FormField.TYPES.TEXT, true, ''),\n new FormField('instructorchoiceacceptgrades', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendname', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorchoicesendemailaddr', FormField.TYPES.CHECKBOX, true, true),\n new FormField('instructorcustomparameters', FormField.TYPES.TEXT, true, ''),\n new FormField('icon', FormField.TYPES.TEXT, true, ''),\n new FormField('secureicon', FormField.TYPES.TEXT, true, ''),\n new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0),\n new FormField('grade_modgrade_point', FormField.TYPES.TEXT, false, ''),\n new FormField('lineitemresourceid', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemtag', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemsubreviewurl', FormField.TYPES.TEXT, true, ''),\n new FormField('lineitemsubreviewparams', FormField.TYPES.TEXT, true, '')\n ];\n\n /**\n * Hide the element, including aria and tab index.\n * @param {HTMLElement} e the element to be hidden.\n */\n const hideElement = (e) => {\n e.setAttribute('hidden', 'true');\n e.setAttribute('aria-hidden', 'true');\n e.setAttribute('tab-index', '-1');\n };\n\n /**\n * Show the element, including aria and tab index (set to 1).\n * @param {HTMLElement} e the element to be shown.\n */\n const showElement = (e) => {\n e.removeAttribute('hidden');\n e.setAttribute('aria-hidden', 'false');\n e.setAttribute('tab-index', '1');\n };\n\n /**\n * When more than one item needs to be added, the UI is simplified\n * to just list the items to be added. Form is hidden and the only\n * options is (save and return to course) or cancel.\n * This function injects the summary to the form page, and hides\n * the unneeded elements.\n * @param {Object[]} items items to be added to the course.\n */\n const showMultipleSummaryAndHideForm = async function(items) {\n const form = document.querySelector('#region-main-box form');\n const toolArea = form.querySelector('[data-attribute=\"dynamic-import\"]');\n const buttonGroup = form.querySelector('#fgroup_id_buttonar');\n const submitAndLaunch = form.querySelector('#id_submitbutton');\n Array.from(form.children).forEach(hideElement);\n hideElement(submitAndLaunch);\n const {html, js} = await templates.renderForPromise('mod_lti/tool_deeplinking_results',\n {items: items});\n\n await templates.replaceNodeContents(toolArea, html, js);\n showElement(toolArea);\n showElement(buttonGroup);\n };\n\n /**\n * Transforms config values aimed at populating the lti mod form to JSON variant\n * which are used to insert more than one activity modules in one submit\n * by applying variation to the submitted form.\n * See /course/modedit.php.\n * @private\n * @param {Object} config transforms a config to an actual form data to be posted.\n * @return {Object} variant that will be used to modify form values on submit.\n */\n var configToVariant = (config) => {\n const variant = {};\n ['name', 'toolurl', 'securetoolurl', 'instructorcustomparameters', 'icon', 'secureicon',\n 'launchcontainer', 'lineitemresourceid', 'lineitemtag', 'lineitemsubreviewurl',\n 'lineitemsubreviewparams'].forEach(\n function(name) {\n variant[name] = config[name] || '';\n }\n );\n variant['introeditor[text]'] = config.introeditor ? config.introeditor.text : '';\n variant['introeditor[format]'] = config.introeditor ? config.introeditor.format : '';\n if (config.instructorchoiceacceptgrades === 1) {\n variant.instructorchoiceacceptgrades = '1';\n variant['grade[modgrade_point]'] = config.grade_modgrade_point || '100';\n } else {\n variant.instructorchoiceacceptgrades = '0';\n }\n return variant;\n };\n\n /**\n * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data.\n * If the return data contains more than one item, the form will not be populated with item data\n * but rather hidden, and the item data will be added to a single input field used to create multiple\n * instances in one request.\n *\n * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue.\n */\n window.processContentItemReturnData = function(returnData) {\n if (dialogue) {\n dialogue.hide();\n }\n var index;\n if (returnData.multiple) {\n for (index in ltiFormFields) {\n // Name is required, so putting a placeholder as it will not be used\n // in multi-items add.\n ltiFormFields[index].setFieldValue(ltiFormFields[index].name === 'name' ? 'item' : null);\n }\n var variants = [];\n returnData.multiple.forEach(function(v) {\n variants.push(configToVariant(v));\n });\n showMultipleSummaryAndHideForm(returnData.multiple);\n const submitAndCourse = document.querySelector('#id_submitbutton2');\n submitAndCourse.onclick = (e) => {\n e.preventDefault();\n submitAndCourse.disabled = true;\n const fd = new FormData(document.querySelector('#region-main-box form'));\n const postVariant = (promise, variant) => {\n Object.entries(variant).forEach((entry) => fd.set(entry[0], entry[1]));\n const body = new URLSearchParams(fd);\n const doPost = () => fetch(document.location.pathname, {method: 'post', body});\n return promise.then(doPost).catch(doPost);\n };\n const backToCourse = () => {\n document.querySelector(\"#id_cancel\").click();\n };\n variants.reduce(postVariant, Promise.resolve()).then(backToCourse).catch(backToCourse);\n };\n } else {\n // Populate LTI configuration fields from return data.\n for (index in ltiFormFields) {\n var field = ltiFormFields[index];\n var value = null;\n if (typeof returnData[field.name] !== 'undefined') {\n value = returnData[field.name];\n }\n field.setFieldValue(value);\n }\n field.setFieldValue(value);\n\n // Update the UI element which signifies content has been selected.\n document.querySelector(\"#id_selectcontentindicator\").innerHTML = returnData.selectcontentindicator;\n }\n\n if (doneCallback) {\n doneCallback(returnData);\n }\n };\n\n return contentItem;\n }\n);\n"],"names":["define","$","notification","str","templates","FormField","Modal","ModalEvents","dialogue","doneCallback","contentItem","init","url","toolUrl","postData","cb","context","bodyPromise","render","setBody","show","get_string","then","title","create","body","large","modal","getRoot","on","hidden","fetchNotifications","catch","exception","ltiFormFields","TYPES","TEXT","EDITOR","CHECKBOX","SELECT","hideElement","e","setAttribute","showElement","removeAttribute","window","processContentItemReturnData","returnData","index","hide","multiple","setFieldValue","name","variants","forEach","v","push","config","variant","introeditor","text","format","instructorchoiceacceptgrades","grade_modgrade_point","configToVariant","async","items","form","document","querySelector","toolArea","buttonGroup","submitAndLaunch","Array","from","children","html","js","renderForPromise","replaceNodeContents","showMultipleSummaryAndHideForm","submitAndCourse","onclick","preventDefault","disabled","fd","FormData","backToCourse","click","reduce","promise","Object","entries","entry","set","URLSearchParams","doPost","fetch","location","pathname","method","Promise","resolve","field","value","innerHTML","selectcontentindicator"],"mappings":";;;;;;;;;;;AA0BAA,6BACI,CACI,SACA,oBACA,WACA,iBACA,qBACA,aACA,sBAEJ,SAASC,EAAGC,aAAcC,IAAKC,UAAWC,UAAWC,MAAOC,iBACpDC,SACAC,aACAC,YAAc,CASdC,KAAM,SAASC,IAAKC,QAASC,SAAUC,IACnCN,aAAeM,OACXC,QAAU,CACVJ,IAAKA,IACLC,QAASA,QACTC,SAAUA,UAEVG,YAAcb,UAAUc,OAAO,sBAAuBF,YAEtDR,gBAEAA,SAASW,QAAQF,kBAEjBT,SAASY,OAIbjB,IAAIkB,WAAW,gBAAiB,OAAOC,MAAK,SAASC,cAC1CjB,MAAMkB,OAAO,CAChBD,MAAOA,MACPE,KAAMR,YACNS,OAAO,EACPN,MAAM,OAEXE,MAAK,SAASK,OACbnB,SAAWmB,MAEXA,MAAMC,UAAUC,GAAGtB,YAAYuB,QAAQ,WAEnCH,MAAMR,QAAQ,IAGdjB,aAAa6B,2BAGlBC,MAAM9B,aAAa+B,aAO1BC,cAAgB,CAChB,IAAI7B,UAAU,OAAQA,UAAU8B,MAAMC,MAAM,EAAO,IACnD,IAAI/B,UAAU,cAAeA,UAAU8B,MAAME,QAAQ,EAAO,IAC5D,IAAIhC,UAAU,UAAWA,UAAU8B,MAAMC,MAAM,EAAM,IACrD,IAAI/B,UAAU,gBAAiBA,UAAU8B,MAAMC,MAAM,EAAM,IAC3D,IAAI/B,UAAU,+BAAgCA,UAAU8B,MAAMG,UAAU,GAAM,GAC9E,IAAIjC,UAAU,2BAA4BA,UAAU8B,MAAMG,UAAU,GAAM,GAC1E,IAAIjC,UAAU,gCAAiCA,UAAU8B,MAAMG,UAAU,GAAM,GAC/E,IAAIjC,UAAU,6BAA8BA,UAAU8B,MAAMC,MAAM,EAAM,IACxE,IAAI/B,UAAU,OAAQA,UAAU8B,MAAMC,MAAM,EAAM,IAClD,IAAI/B,UAAU,aAAcA,UAAU8B,MAAMC,MAAM,EAAM,IACxD,IAAI/B,UAAU,kBAAmBA,UAAU8B,MAAMI,QAAQ,EAAM,GAC/D,IAAIlC,UAAU,uBAAwBA,UAAU8B,MAAMC,MAAM,EAAO,IACnE,IAAI/B,UAAU,qBAAsBA,UAAU8B,MAAMC,MAAM,EAAM,IAChE,IAAI/B,UAAU,cAAeA,UAAU8B,MAAMC,MAAM,EAAM,IACzD,IAAI/B,UAAU,uBAAwBA,UAAU8B,MAAMC,MAAM,EAAM,IAClE,IAAI/B,UAAU,0BAA2BA,UAAU8B,MAAMC,MAAM,EAAM,WAOnEI,YAAeC,IACjBA,EAAEC,aAAa,SAAU,QACzBD,EAAEC,aAAa,cAAe,QAC9BD,EAAEC,aAAa,YAAa,OAO1BC,YAAeF,IACjBA,EAAEG,gBAAgB,UAClBH,EAAEC,aAAa,cAAe,SAC9BD,EAAEC,aAAa,YAAa,aA+DhCG,OAAOC,6BAA+B,SAASC,gBAIvCC,SAHAxC,UACAA,SAASyC,OAGTF,WAAWG,SAAU,KAChBF,SAASd,cAGVA,cAAcc,OAAOG,cAA4C,SAA9BjB,cAAcc,OAAOI,KAAkB,OAAS,UAEnFC,SAAW,GACfN,WAAWG,SAASI,SAAQ,SAASC,GACjCF,SAASG,KAzCEC,CAAAA,eACbC,QAAU,UACf,OAAQ,UAAW,gBAAiB,6BAA8B,OAAQ,aACvE,kBAAmB,qBAAsB,cAAe,uBACxD,2BAA2BJ,SAC3B,SAASF,MACLM,QAAQN,MAAQK,OAAOL,OAAS,MAGxCM,QAAQ,qBAAuBD,OAAOE,YAAcF,OAAOE,YAAYC,KAAO,GAC9EF,QAAQ,uBAAyBD,OAAOE,YAAcF,OAAOE,YAAYE,OAAS,GACtC,IAAxCJ,OAAOK,8BACPJ,QAAQI,6BAA+B,IACvCJ,QAAQ,yBAA2BD,OAAOM,sBAAwB,OAElEL,QAAQI,6BAA+B,IAEpCJ,SAwBeM,CAAgBT,OAjEHU,eAAeC,aAC5CC,KAAOC,SAASC,cAAc,yBAC9BC,SAAWH,KAAKE,cAAc,qCAC9BE,YAAcJ,KAAKE,cAAc,uBACjCG,gBAAkBL,KAAKE,cAAc,oBAC3CI,MAAMC,KAAKP,KAAKQ,UAAUrB,QAAQd,aAClCA,YAAYgC,uBACNI,KAACA,KAADC,GAAOA,UAAYzE,UAAU0E,iBAAiB,mCAChD,CAACZ,MAAOA,cAEN9D,UAAU2E,oBAAoBT,SAAUM,KAAMC,IACpDlC,YAAY2B,UACZ3B,YAAY4B,aAuDRS,CAA+BjC,WAAWG,gBACpC+B,gBAAkBb,SAASC,cAAc,qBAC/CY,gBAAgBC,QAAWzC,IACvBA,EAAE0C,iBACFF,gBAAgBG,UAAW,QACrBC,GAAK,IAAIC,SAASlB,SAASC,cAAc,0BAOzCkB,aAAe,KACjBnB,SAASC,cAAc,cAAcmB,SAEzCnC,SAASoC,QATW,CAACC,QAAShC,WAC1BiC,OAAOC,QAAQlC,SAASJ,SAASuC,OAAUR,GAAGS,IAAID,MAAM,GAAIA,MAAM,YAC5DpE,KAAO,IAAIsE,gBAAgBV,IAC3BW,OAAS,IAAMC,MAAM7B,SAAS8B,SAASC,SAAU,CAACC,OAAQ,OAAQ3E,KAAAA,cACjEiE,QAAQpE,KAAK0E,QAAQhE,MAAMgE,UAKTK,QAAQC,WAAWhF,KAAKiE,cAAcvD,MAAMuD,mBAE1E,KAEEvC,SAASd,cAAe,KACrBqE,MAAQrE,cAAcc,OACtBwD,MAAQ,UAC0B,IAA3BzD,WAAWwD,MAAMnD,QACxBoD,MAAQzD,WAAWwD,MAAMnD,OAE7BmD,MAAMpD,cAAcqD,OAExBD,MAAMpD,cAAcqD,OAGpBpC,SAASC,cAAc,8BAA8BoC,UAAY1D,WAAW2D,uBAG5EjG,cACAA,aAAasC,aAIdrC"}openid-registration.php000064400000006727152265520610011263 0ustar00. /** * This file receives a registration request along with the registration token and returns a client_id. * * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @package mod_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); use mod_lti\local\ltiopenid\registration_helper; use mod_lti\local\ltiopenid\registration_exception; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); $code = 200; $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' or ($_SERVER['REQUEST_METHOD'] === 'GET')) { $doregister = $_SERVER['REQUEST_METHOD'] === 'POST'; // Retrieve registration token from Bearer Authorization header. $authheader = moodle\mod\lti\OAuthUtil::get_headers()['Authorization'] ?? ''; if (!($authheader && substr($authheader, 0, 7) == 'Bearer ')) { $message = 'missing_registration_token'; $code = 401; } else { // Registers tool. try { $tokenres = registration_helper::get()->validate_registration_token(trim(substr($authheader, 7))); $type = new stdClass(); $type->state = LTI_TOOL_STATE_PENDING; if (array_key_exists('type', $tokenres)) { $type = $tokenres['type']; } if ($doregister) { $registrationpayload = json_decode(file_get_contents('php://input'), true); $config = registration_helper::get()->registration_to_config($registrationpayload, $tokenres['clientid']); if ($type->id) { lti_update_type($type, clone $config); $typeid = $type->id; } else { $typeid = lti_add_type($type, clone $config); } header('Content-Type: application/json; charset=utf-8'); $message = json_encode(registration_helper::get()->config_to_registration((object)$config, $typeid)); } else if ($type) { $config = lti_get_type_config($type->id); header('Content-Type: application/json; charset=utf-8'); $message = json_encode(registration_helper::get()->config_to_registration((object)$config, $type->id, $type)); } else { $code = 404; $message = "No registration found."; } } catch (registration_exception $e) { $code = $e->getCode(); $message = $e->getMessage(); } } } else { $code = 400; $message = 'Unsupported operation'; } $response = new \mod_lti\local\ltiservice\response(); // Set code. $response->set_code($code); // Set body. $response->set_body($message); $response->send(); UPGRADING.md000064400000000315152265520610006411 0ustar00# mod_lti Upgrade notes ## 5.0 ### Removed - Final removal of deprecated `lti_libxml_disable_entity_loader` function For more information see [MDL-78635](https://tracker.moodle.org/browse/MDL-78635) coursetooledit.php000064400000006352152265520610010333 0ustar00. /** * Page allowing instructors to configure course-level tools. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\output\notification; require_once('../../config.php'); require_once($CFG->dirroot.'/mod/lti/edit_form.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); $courseid = required_param('course', PARAM_INT); $typeid = optional_param('typeid', null, PARAM_INT); // Permissions etc. require_login($courseid, false); require_capability('mod/lti:addcoursetool', context_course::instance($courseid)); if (!empty($typeid)) { $type = lti_get_type_type_config($typeid); if ($type->course != $courseid || $type->course == get_site()->id) { throw new moodle_exception('You do not have permissions to edit this tool type.'); } } else { $type = (object) ['lti_clientid' => null]; } // Page setup. $url = new moodle_url('/mod/lti/coursetooledit.php', ['courseid' => $courseid]); $pageheading = !empty($typeid) ? get_string('courseexternaltooledit', 'mod_lti', $type->lti_typename) : get_string('courseexternaltooladd', 'mod_lti'); $PAGE->set_url($url); $PAGE->set_pagelayout('incourse'); $PAGE->set_title($pageheading); $PAGE->set_secondary_active_tab('coursetools'); $PAGE->add_body_class('limitedwidth'); $form = new mod_lti_edit_types_form($url, (object)array('id' => $typeid, 'clientid' => $type->lti_clientid, 'iscoursetool' => true)); if ($form->is_cancelled()) { redirect(new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid])); } else if ($data = $form->get_data()) { require_sesskey(); if (!empty($data->typeid)) { $type = (object) ['id' => $data->typeid]; lti_load_type_if_cartridge($data); lti_update_type($type, $data); $redirecturl = new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid]); $notice = get_string('courseexternaltooleditsuccess', 'mod_lti'); } else { $type = (object) ['state' => LTI_TOOL_STATE_CONFIGURED, 'course' => $data->course]; lti_load_type_if_cartridge($data); lti_add_type($type, $data); $redirecturl = new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid]); $notice = get_string('courseexternaltooladdsuccess', 'mod_lti', $type->name); } redirect($redirecturl, $notice, 0, notification::NOTIFY_SUCCESS); } // Display the form. echo $OUTPUT->header(); echo $OUTPUT->heading($pageheading); if (!empty($typeid)) { $form->set_data($type); } $form->display(); echo $OUTPUT->footer(); tests/fixtures/tool_provider.php000064400000001721152265520610013164 0ustar00. /** * Testing fixture. * * @package mod_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ ?> Tool provider

This represents a tool provider

tests/fixtures/ims_cartridge_basic_lti_link.xml000064400000003102152265520610016143 0ustar00 Example tool Example tool description http://download.moodle.org/unittest/test.jpg https://download.moodle.org/unittest/test.jpg http://www.example.com/lti/provider.php https://www.example.com/lti/provider.php http://download.moodle.org/unittest/test.jpg https://download.moodle.org/unittest/test.jpg moodle.org Moodle Moodle Learning Management System Example Cartridge http://www.moodle.org/ moodle@example.com tests/fixtures/test_edit_form.php000064400000002734152265520610013311 0ustar00. /** * Unit tests for mod_lti edit_form * * @package mod_lti * @copyright 2023 Jackson D'Souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/edit_form.php'); /** * Testing fixture. * * @package mod_lti * @copyright 2023 Jackson D'Souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 */ class test_edit_form extends \mod_lti_edit_types_form { /** * Expose the internal moodleform's MoodleQuickForm * * @return MoodleQuickForm */ public function get_quick_form() { return $this->_form; } } tests/generator/lib.php000064400000016150152265520610011162 0ustar00. /** * mod_lti data generator * * @package mod_lti * @category test * @copyright Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com) * @author Mark Nielsen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * LTI module data generator class * * @package mod_lti * @category test * @copyright Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com) * @author Mark Nielsen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_lti_generator extends testing_module_generator { public function create_instance($record = null, ?array $options = null) { $record = (object) (array) $record; if (!isset($record->toolurl)) { $record->toolurl = ''; } else { $toolurl = new moodle_url($record->toolurl); $record->toolurl = $toolurl->out(false); } if (!isset($record->resourcekey)) { $record->resourcekey = '12345'; } if (!isset($record->password)) { $record->password = 'secret'; } if (!isset($record->grade)) { $record->grade = 100; } if (!isset($record->instructorchoicesendname)) { $record->instructorchoicesendname = 1; } if (!isset($record->instructorchoicesendemailaddr)) { $record->instructorchoicesendemailaddr = 1; } if (!isset($record->instructorchoiceacceptgrades)) { $record->instructorchoiceacceptgrades = 1; } if (!isset($record->typeid)) { $record->typeid = 0; } return parent::create_instance($record, (array)$options); } /** * Create a tool proxy. * * @param array $config */ public function create_tool_proxies(array $config) { if (!isset($config['capabilityoffered'])) { $config['capabilityoffered'] = ''; } if (!isset($config['serviceoffered'])) { $config['serviceoffered'] = ''; } lti_add_tool_proxy((object) $config); } /** * Split type creation data into 'type' and 'config' components, based on input array key prefixes. * * The $data array contains both the type data and config data that will be passed to lti_add_type(). This must be split into * two params (type, config) based on the array key prefixes ({@see lti_add_type()} for how the two params are handled): * - NO prefix: denotes 'type' data. * - 'lti_' prefix: denotes 'config' data. * - 'ltiservice_' prefix: denotes 'config' data, specifically config for service plugins. * * @param array $data array of type and config data containing prefixed keys. * @return array containing separated objects for type and config data. E.g. ['type' = stdClass, 'config' => stdClass] */ protected function get_type_and_config_from_data(array $data): array { // Grab any non-prefixed fields; these are the type fields. The rest is considered config. $type = array_filter( $data, fn($val, $key) => !str_contains($key, 'lti_') && !str_contains($key, 'ltiservice_'), ARRAY_FILTER_USE_BOTH ); $config = array_diff_key($data, $type); return ['type' => (object) $type, 'config' => (object) $config]; } /** * Create a tool type. * * @param array $data * @return int ID of created tool */ public function create_tool_types(array $data): int { if (!isset($data['baseurl'])) { throw new coding_exception('Must specify baseurl when creating a LTI tool type.'); } $data['baseurl'] = (new moodle_url($data['baseurl']))->out(false); // Permits relative URLs in behat features. // Sensible defaults permitting the tool type to be used in a launch. $data['lti_acceptgrades'] = $data['lti_acceptgrades'] ?? LTI_SETTING_ALWAYS; $data['lti_sendname'] = $data['lti_sendname'] ?? LTI_SETTING_ALWAYS; $data['lti_sendemailaddr'] = $data['lti_sendname'] ?? LTI_SETTING_ALWAYS; $data['lti_launchcontainer'] = $data['lti_launchcontainer'] ?? LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; ['type' => $type, 'config' => $config] = $this->get_type_and_config_from_data($data); return lti_add_type(type: $type, config: $config); } /** * Create a course tool type. * * @param array $type the type info. * @return int ID of created tool. * @throws coding_exception if any required fields are missing. */ public function create_course_tool_types(array $type): int { global $SITE; if (!isset($type['baseurl'])) { throw new coding_exception('Must specify baseurl when creating a course tool type.'); } if (!isset($type['course']) || $type['course'] == $SITE->id) { throw new coding_exception('Must specify a non-site course when creating a course tool type.'); } $type['baseurl'] = (new moodle_url($type['baseurl']))->out(false); // Permits relative URLs in behat features. $type['coursevisible'] = $type['coursevisible'] ?? LTI_COURSEVISIBLE_ACTIVITYCHOOSER; $type['state'] = LTI_TOOL_STATE_CONFIGURED; // The default for course tools. // Sensible defaults permitting the tool type to be used in a launch. $type['lti_acceptgrades'] = $type['lti_acceptgrades'] ?? LTI_SETTING_ALWAYS; $type['lti_sendname'] = $type['lti_sendname'] ?? LTI_SETTING_ALWAYS; $type['lti_sendemailaddr'] = $type['lti_sendemailaddr'] ?? LTI_SETTING_ALWAYS; $type['lti_coursevisible'] = $type['coursevisible'] ?? LTI_COURSEVISIBLE_ACTIVITYCHOOSER; $type['lti_launchcontainer'] = $type['lti_launchcontainer'] ?? LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; // Required for cartridge processing support. $type['lti_toolurl'] = $type['baseurl']; $type['lti_description'] = $type['description'] ?? ''; $type['lti_icon'] = $type['icon'] ?? ''; $type['lti_secureicon'] = $type['secureicon'] ?? ''; if (!empty($type['name'])) { $type['lti_typename'] = $type['name']; } ['type' => $type, 'config' => $config] = $this->get_type_and_config_from_data($type); lti_load_type_if_cartridge($config); return lti_add_type(type: $type, config: $config); } } tests/generator/behat_mod_lti_generator.php000064400000007310152265520610015252 0ustar00. defined('MOODLE_INTERNAL') || die(); /** * Define behat generator for mod_lti. * * @package mod_lti * @category test * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_lti_generator extends behat_generator_base { /** * Get list of entities for mod_lti behat tests. * * @return array[] List of entity definitions. */ protected function get_creatable_entities(): array { return [ 'tool proxies' => [ 'singular' => 'tool proxy', 'datagenerator' => 'tool_proxies', 'required' => [], ], 'tool types' => [ 'singular' => 'tool type', 'datagenerator' => 'tool_types', 'required' => ['baseurl'], 'switchids' => ['lti_coursecategories' => 'lti_coursecategories'] ], 'course tools' => [ 'singular' => 'course tool', 'datagenerator' => 'course_tool_types', 'required' => ['baseurl', 'course'], 'switchids' => ['course' => 'course'] ], 'tool instances' => [ 'singular' => 'instance', 'datagenerator' => 'instance', 'required' => ['course', 'tool'], 'switchids' => ['course' => 'course', 'tool' => 'typeid'] ], ]; } /** * Handles the switchid ['tool' => 'typeid'] for finding a tool by name. * * @param string $name the name of the tool. * @return int the id of the tool type identified by the name $name. */ protected function get_tool_id(string $name): int { global $DB; if (!$id = $DB->get_field('lti_types', 'id', ['name' => $name])) { throw new coding_exception('The specified tool with name "' . $name . '" does not exist'); } return (int) $id; } /** * Handles the switchid ['lti_coursecategories' => 'lti_coursecategories'] for restricting a tool to certain categories. * * @param string $idnumbers a comma-separated string containing the course category id numbers, e.g. 'cata, catb, catc'. * @return string a comma-separated string containing the course category ids. * @throws coding_exception if one or more of the categories is unable to be matched by its idnumber. */ protected function get_lti_coursecategories_id(string $idnumbers): string { global $DB; $categoryids = array_map('trim', explode(',', $idnumbers)); [$insql, $inparams] = $DB->get_in_or_equal($categoryids); $ids = $DB->get_fieldset_sql("SELECT id FROM {course_categories} WHERE idnumber $insql", $inparams); if (!$ids || count($ids) != count($categoryids)) { throw new coding_exception("One or more course categories unable to be matched using idnumbers: $idnumbers"); } return implode(',', $ids); } } tests/external/toggle_showinactivitychooser_test.php000064400000016567152265520610017333 0ustar00. namespace mod_lti\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * PHPUnit tests for toggle_showinactivitychooser external function. * * @package mod_lti * @copyright 2023 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\external\toggle_showinactivitychooser */ final class toggle_showinactivitychooser_test extends \mod_lti_testcase { /** * Test toggle_showinactivitychooser for course tool. * @covers ::execute */ public function test_toggle_showinactivitychooser_course_tool(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($editingteacher); $typeid = lti_add_type( (object) [ 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => $course->id, 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER ], (object) [ 'lti_typename' => "My course tool", 'lti_toolurl' => 'http://example.com', 'lti_ltiversion' => 'LTI-1p0', 'lti_coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER ] ); $result = toggle_showinactivitychooser::execute($typeid, $course->id, false); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertTrue($result); $sql = "SELECT lt.coursevisible coursevisible FROM {lti_types} lt WHERE lt.id = ?"; $actual = $DB->get_record_sql($sql, [$typeid]); $this->assertEquals(LTI_COURSEVISIBLE_PRECONFIGURED, $actual->coursevisible); $result = toggle_showinactivitychooser::execute($typeid, $course->id, true); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertTrue($result); $actual = $DB->get_record_sql($sql, [$typeid]); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $actual->coursevisible); } /** * Test toggle_showinactivitychooser for site tool. * @covers ::execute */ public function test_toggle_showinactivitychooser_site_tool(): void { global $DB; $this->resetAfterTest(); $coursecat1 = $this->getDataGenerator()->create_category(); $coursecat2 = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]); $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($editingteacher); $type = $this->generate_tool_type(123); // Creates a site tool. $result = toggle_showinactivitychooser::execute($type->id, $course->id, false); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertTrue($result); $sql = "SELECT lt.coursevisible coursevisible1, lc.coursevisible AS coursevisible2 FROM {lti_types} lt LEFT JOIN {lti_coursevisible} lc ON lt.id = lc.typeid WHERE lt.id = ? AND lc.courseid = ?"; $actual = $DB->get_record_sql($sql, [$type->id, $course->id]); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $actual->coursevisible1); $this->assertEquals(LTI_COURSEVISIBLE_PRECONFIGURED, $actual->coursevisible2); $result = toggle_showinactivitychooser::execute($type->id, $course->id, true); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertTrue($result); $actual = $DB->get_record_sql($sql, [$type->id, $course->id]); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $actual->coursevisible1); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $actual->coursevisible2); $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 1', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat1->id ]); $tool = $DB->get_record('lti_types', ['name' => 'site tool preconfigured and activity chooser, restricted to category 1']); $result = toggle_showinactivitychooser::execute($tool->id, $course->id, false); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertTrue($result); $actual = $DB->get_record_sql($sql, [$tool->id, $course->id]); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $actual->coursevisible1); $this->assertEquals(LTI_COURSEVISIBLE_PRECONFIGURED, $actual->coursevisible2); $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 2', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); $tool = $DB->get_record('lti_types', ['name' => 'site tool preconfigured and activity chooser, restricted to category 2']); $this->expectException('moodle_exception'); $this->expectExceptionMessage('You are not allowed to change this setting for this tool.'); toggle_showinactivitychooser::execute($tool->id, $course->id, true); $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $ltigenerator->create_tool_types([ 'name' => 'site tool dont show', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_NO, 'state' => LTI_TOOL_STATE_CONFIGURED, ]); $tool = $DB->get_record('lti_types', ['name' => 'site tool dont show']); $result = toggle_showinactivitychooser::execute($tool->id, $course->id, false); $result = external_api::clean_returnvalue(toggle_showinactivitychooser::execute_returns(), $result); $this->assertFalse($result); } } tests/external/get_tool_types_and_proxies_count_test.php000064400000004645152265520610020160 0ustar00. namespace mod_lti\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * PHPUnit tests for get_tool_types_and_proxies_count external function. * * @package mod_lti * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_tool_types_and_proxies_count_test extends \mod_lti_testcase { /** * This method runs before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); } /** * Test get_tool_types_and_proxies_count returns the correct number. */ public function test_mod_lti_get_tool_types_and_proxies_count(): void { for ($i = 0; $i < 10; $i++) { $proxy = $this->generate_tool_proxy($i); $this->generate_tool_type($i, $proxy->id); } $data = \mod_lti\external\get_tool_types_and_proxies_count::execute(0, false); $data = external_api::clean_returnvalue(\mod_lti\external\get_tool_types_and_proxies_count::execute_returns(), $data); $this->assertEquals(20, $data['count']); } /** * Test get_tool_types_and_proxies_count returns the correct number. */ public function test_mod_lti_get_tool_types_and_proxies_count_with_no_tools_configured(): void { $data = \mod_lti\external\get_tool_types_and_proxies_count::execute(0, false); $data = external_api::clean_returnvalue(\mod_lti\external\get_tool_types_and_proxies_count::execute_returns(), $data); $this->assertEquals(0, $data['count']); } } tests/external/delete_course_tool_type_test.php000064400000005363152265520610016233 0ustar00. namespace mod_lti\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * PHPUnit tests for delete_course_tool_type external function. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\external\delete_course_tool_type */ final class delete_course_tool_type_test extends \mod_lti_testcase { /** * Test delete_course_tool() for a course tool. * @covers ::execute */ public function test_delete_course_tool(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($editingteacher); $typeid = lti_add_type( (object) [ 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => $course->id ], (object) [ 'lti_typename' => "My course tool", 'lti_toolurl' => 'http://example.com', 'lti_ltiversion' => 'LTI-1p0' ] ); $data = delete_course_tool_type::execute($typeid); $data = external_api::clean_returnvalue(delete_course_tool_type::execute_returns(), $data); $this->assertTrue($data); } /** * Test delete_course_tool() for a site tool, which is forbidden. * @covers ::execute */ public function test_delete_course_tool_site_tool(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($editingteacher); $type = $this->generate_tool_type(123); // Creates a site tool. $this->expectException(\invalid_parameter_exception::class); delete_course_tool_type::execute($type->id); } } tests/external/get_tool_types_and_proxies_test.php000064400000010333152265520610016737 0ustar00. namespace mod_lti\external; use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * PHPUnit tests for get_tool_types_and_proxies external function. * * @package mod_lti * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_tool_types_and_proxies_test extends \mod_lti_testcase { /** * This method runs before every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); } /** * Test get_tool_types_and_proxies. */ public function test_mod_lti_get_tool_types_and_proxies(): void { $proxy = $this->generate_tool_proxy(1); $this->generate_tool_type(1, $proxy->id); $data = get_tool_types_and_proxies::execute(0, false, 50, 0); $data = external_api::clean_returnvalue(get_tool_types_and_proxies::execute_returns(), $data); $this->assertCount(1, $data['types']); $type = $data['types'][0]; $this->assertEquals('Test tool 1', $type['name']); $this->assertEquals('Example description 1', $type['description']); $this->assertCount(1, $data['proxies']); $proxy = $data['proxies'][0]; $this->assertEquals('Test proxy 1', $proxy['name']); $this->assertEquals(50, $data['limit']); $this->assertEquals(0, $data['offset']); } /** * Test get_tool_types_and_proxies with multiple pages of tool types. */ public function test_mod_lti_get_tool_types_and_proxies_with_multiple_pages(): void { for ($i = 0; $i < 3; $i++) { $proxy = $this->generate_tool_proxy($i); $this->generate_tool_type($i, $proxy->id); } $data = get_tool_types_and_proxies::execute(0, false, 5, 0); $data = external_api::clean_returnvalue(get_tool_types_and_proxies::execute_returns(), $data); $this->assertCount(2, $data['types']); $this->assertCount(3, $data['proxies']); $this->assertEquals(5, $data['limit']); $this->assertEquals(0, $data['offset']); } /** * Test get_tool_types_and_proxies with multiple pages of tool types and offset. */ public function test_mod_lti_get_tool_types_and_proxies_with_multiple_pages_last_page(): void { for ($i = 0; $i < 6; $i++) { $proxy = $this->generate_tool_proxy($i); $this->generate_tool_type($i, $proxy->id); } $data = get_tool_types_and_proxies::execute(0, false, 5, 10); $data = external_api::clean_returnvalue(get_tool_types_and_proxies::execute_returns(), $data); $this->assertCount(2, $data['types']); $this->assertCount(0, $data['proxies']); $this->assertEquals(5, $data['limit']); $this->assertEquals(10, $data['offset']); } /** * Test get_tool_types_and_proxies without pagination. */ public function test_mod_lti_get_tool_types_and_proxies_without_pagination(): void { for ($i = 0; $i < 10; $i++) { $proxy = $this->generate_tool_proxy($i); $this->generate_tool_type($i, $proxy->id); } $data = get_tool_types_and_proxies::execute(0, false, 0, 0); $data = external_api::clean_returnvalue(get_tool_types_and_proxies::execute_returns(), $data); $this->assertCount(10, $data['types']); $this->assertCount(10, $data['proxies']); } } tests/local/ltiopenid/registration_test.php000064400000053677152265520610015277 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. namespace mod_lti\local\ltiopenid; /** * OpenId LTI Registration library tests * * @package mod_lti * @copyright 2020 Claude Vervoort, Cengage * @author Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class registration_test extends \advanced_testcase { /** * @var string A has-it-all client registration. */ private $registrationfulljson = <<registrationfulljson, true); $registration['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; $config = registration_helper::get()->registration_to_config($registration, 'TheClientId'); $this->assertEquals('JWK_KEYSET', $config->lti_keytype); $this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion); $this->assertEquals('TheClientId', $config->lti_clientid); $this->assertEquals('Virtual Garden', $config->lti_typename); $this->assertEquals('Learn Botany by tending to your little (virtual) garden.', $config->lti_description); $this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin); $this->assertEquals(implode(PHP_EOL, ["https://client.example.org/callback", "https://client.example.org/callback2"]), $config->lti_redirectionuris); $this->assertEquals("context_history=\$Context.id.history", $config->lti_customparameters); $this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset); $this->assertEquals("https://client.example.org/logo.png", $config->lti_icon); $this->assertEquals(2, $config->ltiservice_gradesynchronization); $this->assertEquals(LTI_SETTING_DELEGATE, $config->lti_acceptgrades); $this->assertEquals(1, $config->ltiservice_memberships); $this->assertEquals(0, $config->ltiservice_toolsettings); $this->assertEquals('client.example.org', $config->lti_tooldomain); $this->assertEquals('https://client.example.org/lti', $config->lti_toolurl); $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendname); $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendemailaddr); $this->assertEquals(1, $config->lti_contentitem); $this->assertEquals('https://client.example.org/lti/dl', $config->lti_toolurl_ContentItemSelectionRequest); } /** * Test the mapping from Registration JSON to LTI Config for a minimal tool registration. */ public function test_to_config_minimal(): void { $registration = json_decode($this->registrationminimaljson, true); $config = registration_helper::get()->registration_to_config($registration, 'TheClientId'); $this->assertEquals('JWK_KEYSET', $config->lti_keytype); $this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion); $this->assertEquals('TheClientId', $config->lti_clientid); $this->assertEquals('Virtual Garden', $config->lti_typename); $this->assertEmpty($config->lti_description); // Special case here where Moodle ignores www for domains. $this->assertEquals('example.org', $config->lti_tooldomain); $this->assertEquals('https://www.example.org/lti', $config->lti_toolurl); $this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin); $this->assertEquals('https://client.example.org/callback', $config->lti_redirectionuris); $this->assertEmpty($config->lti_customparameters); $this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset); $this->assertEmpty($config->lti_icon); $this->assertEquals(0, $config->ltiservice_gradesynchronization); $this->assertEquals(LTI_SETTING_NEVER, $config->lti_acceptgrades); $this->assertEquals(0, $config->ltiservice_memberships); $this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendname); $this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendemailaddr); $this->assertEquals(0, $config->lti_contentitem); } /** * Test the mapping from Registration JSON to LTI Config for a minimal tool with * deep linking support registration. */ public function test_to_config_minimal_with_deeplinking(): void { $registration = json_decode($this->registrationminimaldljson, true); $config = registration_helper::get()->registration_to_config($registration, 'TheClientId'); $this->assertEquals(1, $config->lti_contentitem); $this->assertEmpty($config->lti_toolurl_ContentItemSelectionRequest); } /** * Validation Test: initiation login. */ public function test_validation_initlogin(): void { $registration = json_decode($this->registrationfulljson, true); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); unset($registration['initiate_login_uri']); registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: redirect uris. */ public function test_validation_redirecturis(): void { $registration = json_decode($this->registrationfulljson, true); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); unset($registration['redirect_uris']); registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: jwks uri empty. */ public function test_validation_jwks(): void { $registration = json_decode($this->registrationfulljson, true); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); $registration['jwks_uri'] = ''; registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: no domain nor targetlinkuri is rejected. */ public function test_validation_missing_domain_targetlinkuri(): void { $registration = json_decode($this->registrationminimaljson, true); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']); unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']); registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: mismatch between domain and targetlinkuri is rejected. */ public function test_validation_domain_targetlinkuri_match(): void { $registration = json_decode($this->registrationminimaljson, true); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); $registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain'] = 'not.the.right.domain'; registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: domain is required. */ public function test_validation_domain_targetlinkuri_onlylink(): void { $registration = json_decode($this->registrationminimaljson, true); unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']); $this->expectException(registration_exception::class); $this->expectExceptionCode(400); $config = registration_helper::get()->registration_to_config($registration, 'TheClientId'); } /** * Validation Test: base url (targetlinkuri) is built from domain if not present. */ public function test_validation_domain_targetlinkuri_onlydomain(): void { $registration = json_decode($this->registrationminimaljson, true); unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']); $config = registration_helper::get()->registration_to_config($registration, 'TheClientId'); $this->assertEquals('example.org', $config->lti_tooldomain); $this->assertEquals('https://www.example.org', $config->lti_toolurl); } /** * Test the transformation from lti config to OpenId LTI Client Registration response. */ public function test_config_to_registration(): void { $orig = json_decode($this->registrationfulljson, true); $orig['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; $reghelper = registration_helper::get(); $reg = $reghelper->config_to_registration($reghelper->registration_to_config($orig, 'clid'), 12); $this->assertEquals('clid', $reg['client_id']); $this->assertEquals($orig['response_types'], $reg['response_types']); $this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']); $this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']); $this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']); $this->assertEquals($orig['logo_uri'], $reg['logo_uri']); $this->assertEquals('https://purl.imsglobal.org/spec/lti-ags/scope/score '. 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly '. 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly '. 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem '. 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', $reg['scope']); $ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration']; $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration']; $this->assertEquals("12", $lti['deployment_id']); $this->assertEquals($ltiorig['target_link_uri'], $lti['target_link_uri']); $this->assertEquals($ltiorig['domain'], $lti['domain']); $this->assertEquals($ltiorig['custom_parameters'], $lti['custom_parameters']); $this->assertEquals($ltiorig['description'], $lti['description']); $dlmsgorig = $ltiorig['messages'][0]; $dlmsg = $lti['messages'][0]; $this->assertEquals($dlmsgorig['type'], $dlmsg['type']); $this->assertEquals($dlmsgorig['target_link_uri'], $dlmsg['target_link_uri']); $this->assertTrue(in_array('iss', $lti['claims'])); $this->assertTrue(in_array('sub', $lti['claims'])); $this->assertTrue(in_array('email', $lti['claims'])); $this->assertTrue(in_array('family_name', $lti['claims'])); $this->assertTrue(in_array('given_name', $lti['claims'])); $this->assertTrue(in_array('name', $lti['claims'])); } /** * Test the transformation from lti config to OpenId LTI Client Registration response for the minimal version. */ public function test_config_to_registration_minimal(): void { $orig = json_decode($this->registrationminimaljson, true); $reghelper = registration_helper::get(); $reg = $reghelper->config_to_registration($reghelper->registration_to_config($orig, 'clid'), 12); $this->assertEquals('clid', $reg['client_id']); $this->assertEquals($orig['response_types'], $reg['response_types']); $this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']); $this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']); $this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']); $this->assertEquals('', $reg['scope']); $ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration']; $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration']; $this->assertTrue(in_array('iss', $lti['claims'])); $this->assertTrue(in_array('sub', $lti['claims'])); $this->assertFalse(in_array('email', $lti['claims'])); $this->assertFalse(in_array('family_name', $lti['claims'])); $this->assertFalse(in_array('given_name', $lti['claims'])); $this->assertFalse(in_array('name', $lti['claims'])); } /** * Test the transformation from lti config 1.1 to Registration Response. */ public function test_config_to_registration_lti11(): void { $config = []; $config['contentitem'] = 1; $config['toolurl_ContentItemSelectionRequest'] = ''; $config['sendname'] = 0; $config['sendemailaddr'] = 1; $config['acceptgrades'] = 2; $config['resourcekey'] = 'testkey'; $config['password'] = 'testp@ssw0rd'; $config['customparameters'] = 'a1=b1'; $type = []; $type['id'] = 130; $type['name'] = 'LTI Test 1.1'; $type['baseurl'] = 'https://base.test.url/test'; $type['tooldomain'] = 'base.test.url'; $type['ltiversion'] = 'LTI-1p0'; $type['icon'] = 'https://base.test.url/icon.png'; $reg = registration_helper::get()->config_to_registration((object)$config, $type['id'], (object)$type); $this->assertFalse(isset($reg['client_id'])); $this->assertFalse(isset($reg['initiate_login_uri'])); $this->assertEquals($type['name'], $reg['client_name']); $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration']; $this->assertEquals(LTI_VERSION_1, $lti['version']); $this->assertEquals('b1', $lti['custom_parameters']['a1']); $this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']); $this->assertEquals('base.test.url', $lti['domain']); $this->assertEquals($type['baseurl'], $lti['target_link_uri']); $oauth = $lti['oauth_consumer']; $this->assertEquals('testkey', $oauth['key']); $this->assertFalse(empty($oauth['nonce'])); $this->assertEquals(hash('sha256', 'testkeytestp@ssw0rd'.$oauth['nonce']), $oauth['sign']); $this->assertTrue(in_array('iss', $lti['claims'])); $this->assertTrue(in_array('sub', $lti['claims'])); $this->assertTrue(in_array('email', $lti['claims'])); $this->assertFalse(in_array('family_name', $lti['claims'])); $this->assertFalse(in_array('given_name', $lti['claims'])); $this->assertFalse(in_array('name', $lti['claims'])); } /** * Test the transformation from lti config 2.0 to Registration Response. * For LTI 2.0 we limit to just passing the previous key/secret. */ public function test_config_to_registration_lti20(): void { $config = []; $config['contentitem'] = 1; $config['toolurl_ContentItemSelectionRequest'] = ''; $type = []; $type['id'] = 131; $type['name'] = 'LTI Test 1.2'; $type['baseurl'] = 'https://base.test.url/test'; $type['tooldomain'] = 'base.test.url'; $type['ltiversion'] = 'LTI-2p0'; $type['icon'] = 'https://base.test.url/icon.png'; $type['toolproxyid'] = 9; $toolproxy = []; $toolproxy['id'] = 9; $toolproxy['guid'] = 'lti2guidtest'; $toolproxy['secret'] = 'peM7YDx420bo'; $reghelper = $this->getMockBuilder(registration_helper::class) ->onlyMethods(['get_tool_proxy']) ->getMock(); $map = [[$toolproxy['id'], $toolproxy]]; $reghelper->method('get_tool_proxy') ->will($this->returnValueMap($map)); $reg = $reghelper->config_to_registration((object)$config, $type['id'], (object)$type); $this->assertFalse(isset($reg['client_id'])); $this->assertFalse(isset($reg['initiate_login_uri'])); $this->assertEquals($type['name'], $reg['client_name']); $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration']; $this->assertEquals(LTI_VERSION_2, $lti['version']); $this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']); $this->assertEquals('base.test.url', $lti['domain']); $this->assertEquals($type['baseurl'], $lti['target_link_uri']); $oauth = $lti['oauth_consumer']; $this->assertEquals('lti2guidtest', $toolproxy['guid']); $this->assertFalse(empty($oauth['nonce'])); $this->assertEquals(hash('sha256', 'lti2guidtestpeM7YDx420bo'.$oauth['nonce']), $oauth['sign']); $this->assertTrue(in_array('iss', $lti['claims'])); $this->assertTrue(in_array('sub', $lti['claims'])); $this->assertFalse(in_array('email', $lti['claims'])); $this->assertFalse(in_array('family_name', $lti['claims'])); $this->assertFalse(in_array('given_name', $lti['claims'])); $this->assertFalse(in_array('name', $lti['claims'])); } } tests/local/ltiopenid/jwks_helper_test.php000064400000027226152265520610015071 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. namespace mod_lti\local\ltiopenid; /** * Tests for the jwks_helper class. * * @coversDefaultClass \mod_lti\local\ltiopenid\jwks_helper * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class jwks_helper_test extends \basic_testcase { /** * Test the fix_jwks_alg method with a range of inputs. * * @dataProvider jwks_alg_provider * @covers ::fix_jwks_alg * @param array $jwks the JWKS key set. * @param string $jwt the JWT. * @param array $expected the expected outputs/exceptions. * @return void */ public function test_fix_jwks_alg(array $jwks, string $jwt, array $expected): void { if (isset($expected['exception'])) { $this->expectException($expected['exception']); } $fixed = jwks_helper::fix_jwks_alg($jwks, $jwt); $this->assertEquals($expected['jwks'], $fixed); } /** * Provider for test_fix_jwks_alg. * @return array test data. */ public static function jwks_alg_provider(): array { return [ // Algs already present, so no changes to input key array. 'All JWKS keys have algs set' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', 'alg' => 'RS256' ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', 'alg' => 'RS256' ] ] ], // RS256 JWT with kid = 42. 'jwt' => 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjQyIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRoZSBjYXQiLCJz'. 'bG9nYW4iOiJMb3ZlcyBpdCwgbG92ZXMgaXQsIGxvdmVzIE1vb2RsZSIsImlhdCI6MTUxNjIzOTAyMn0.EiqMEqufKJj74JevdTxXqzHvHGIcZ'. 'EFYhOe9sliL2FmlyiJcf7waObO2ZNwWvVZwTI4DfEGFamheMmTb6-YBODacDvH6BlQNb0H_6ye6AGl1u-3OAQj7i_SKsLuB37k6Lw5YFrwQYr'. '7bjujSaQx6BL3kaqkqCdZhFjr2EYcn5-NehGHsevKqpMA-ShBovcndYkD5gfZEbXr59sgpQuJ43qO7gnGPzRbaJAEw_0_6v0r3y0pzDNfarNd'. 'fHfCZQbcF9T8dpHAeO4JMmuCanV8iJziI8ihVPwH-BwUJmzthyUgy8542FinHVbXo-88wu9xpbdV17VPgeGGBCpYpnVnWaA', 'expected' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', 'alg' => 'RS256' ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', 'alg' => 'RS256' ] ] ] ] ], // Only the key matching the kid in the JWT header should be fixed. 'All JWKS keys missing alg' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', ] ] ], // RS256 JWT with kid = 42. 'jwt' => 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjQyIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRoZSBjYXQiLCJz'. 'bG9nYW4iOiJMb3ZlcyBpdCwgbG92ZXMgaXQsIGxvdmVzIE1vb2RsZSIsImlhdCI6MTUxNjIzOTAyMn0.EiqMEqufKJj74JevdTxXqzHvHGIcZ'. 'EFYhOe9sliL2FmlyiJcf7waObO2ZNwWvVZwTI4DfEGFamheMmTb6-YBODacDvH6BlQNb0H_6ye6AGl1u-3OAQj7i_SKsLuB37k6Lw5YFrwQYr'. '7bjujSaQx6BL3kaqkqCdZhFjr2EYcn5-NehGHsevKqpMA-ShBovcndYkD5gfZEbXr59sgpQuJ43qO7gnGPzRbaJAEw_0_6v0r3y0pzDNfarNd'. 'fHfCZQbcF9T8dpHAeO4JMmuCanV8iJziI8ihVPwH-BwUJmzthyUgy8542FinHVbXo-88wu9xpbdV17VPgeGGBCpYpnVnWaA', 'expected' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', 'alg' => 'RS256' ] ] ] ] ], // Exception expected when JWT alg is supported but does not match the family of key in the JWK. 'JWT kty algorithm family mismatch' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', ] ] ], // ES256 JWT with kid = 42. 'jwt' => 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjQyIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwi'. 'YWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dbUlZopFo7164JVLD0G4GoZOhoMYWhIXkgtlblBXT6fC3K4lJ38l3LzlEBhfRRKvJlXpe'. 'NNGmBg8V29jd5J33Q', 'expected' => [ 'exception' => \moodle_exception::class ] ], // Exception expected when JWK kid field missing. 'JWT missing kid header field' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', 'alg' => 'RS256' ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', 'alg' => 'RS256' ] ] ], // RS256 JWT with kid omitted. 'jwt' => 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWU'. 'sImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4'. 'SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygT'. 'qVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9Riwr'. 'V7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ', 'expected' => [ 'exception' => \moodle_exception::class ] ], // Exception expected when JWT passes unsupported symmetrical alg. 'JWT passes in unsupported alg' => [ 'jwks' => [ 'keys' => [ [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '41', ], [ 'kty' => 'RSA', 'use' => 'sig', 'e' => 'AQAB', 'n' => '3nVf6', 'kid' => '42', ] ] ], // HS256 JWT with kid = 42. 'jwt' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjQyIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRoZSBjYXQiLCJz'. 'bG9nYW4iOiJMb3ZlcyBpdCwgbG92ZXMgaXQsIGxvdmVzIE1vb2RsZSIsImlhdCI6MTUxNjIzOTAyMn0.zBM5Jw0BOig5-C1R7TD-TzH1QVmyD'. 'yMjbK0KGG76xIE', 'expected' => [ 'exception' => \moodle_exception::class ] ], ]; } } tests/local/types_helper_test.php000064400000031677152265520610013275 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. namespace mod_lti\local; use mod_lti_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * Types helper tests. * * @package mod_lti * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_lti\local\types_helper */ final class types_helper_test extends mod_lti_testcase { /** * Test fetching tool types for a given course and user. * * @covers ::get_lti_types_by_course * @return void. */ public function test_get_lti_types_by_course(): void { $this->resetAfterTest(); global $DB; $coursecat1 = $this->getDataGenerator()->create_category(); $coursecat2 = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]); $course2 = $this->getDataGenerator()->create_course(['category' => $coursecat2->id]); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); $this->setUser($teacher); // Create the following tool types for testing: // - Site tool configured as "Do not show" (LTI_COURSEVISIBLE_NO). // - Site tool configured as "Show as a preconfigured tool only" (LTI_COURSEVISIBLE_PRECONFIGURED). // - Site tool configured as "Show as a preconfigured tool and in the activity chooser" (LTI_COURSEVISIBLE_ACTIVITYCHOOSER). // - Course tool which, by default, is configured as LTI_COURSEVISIBLE_ACTIVITYCHOOSER). // - Site tool configured to "Show as a preconfigured tool and in the activity chooser" but restricted to a category. /** @var \mod_lti_generator $ltigenerator */ $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $ltigenerator->create_tool_types([ 'name' => 'site tool do not show', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_NO, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured only', 'baseurl' => 'http://example.com/tool/2', 'coursevisible' => LTI_COURSEVISIBLE_PRECONFIGURED, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser', 'baseurl' => 'http://example.com/tool/3', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_course_tool_types([ 'name' => 'course tool preconfigured and activity chooser', 'baseurl' => 'http://example.com/tool/4', 'course' => $course->id ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 2', 'baseurl' => 'http://example.com/tool/5', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); // Request using the default 'coursevisible' param will include all tools except the one configured as "Do not show" and // the tool restricted to category 2. $coursetooltypes = types_helper::get_lti_types_by_course($course->id, $teacher->id); $this->assertCount(3, $coursetooltypes); $expected = [ 'http://example.com/tool/2', 'http://example.com/tool/3', 'http://example.com/tool/4', ]; sort($expected); $actual = array_column($coursetooltypes, 'baseurl'); sort($actual); $this->assertEquals($expected, $actual); // Request for only those tools configured to show in the activity chooser for the teacher. $coursetooltypes = types_helper::get_lti_types_by_course($course->id, $teacher->id, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]); $this->assertCount(2, $coursetooltypes); $expected = [ 'http://example.com/tool/3', 'http://example.com/tool/4', ]; sort($expected); $actual = array_column($coursetooltypes, 'baseurl'); sort($actual); $this->assertEquals($expected, $actual); // Request for only those tools configured to show as a preconfigured tool for the teacher. $coursetooltypes = types_helper::get_lti_types_by_course($course->id, $teacher->id, [LTI_COURSEVISIBLE_PRECONFIGURED]); $this->assertCount(1, $coursetooltypes); $expected = [ 'http://example.com/tool/2', ]; $actual = array_column($coursetooltypes, 'baseurl'); $this->assertEquals($expected, $actual); // Request for teacher2 in course2 (course category 2). $coursetooltypes = types_helper::get_lti_types_by_course($course2->id, $teacher2->id); $this->assertCount(3, $coursetooltypes); $expected = [ 'http://example.com/tool/2', 'http://example.com/tool/3', 'http://example.com/tool/5', ]; sort($expected); $actual = array_column($coursetooltypes, 'baseurl'); sort($actual); $this->assertEquals($expected, $actual); // Request for a teacher who cannot use preconfigured tools in the course. $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); assign_capability('mod/lti:addpreconfiguredinstance', CAP_PROHIBIT, $teacherrole->id, \core\context\course::instance($course->id)); $coursetooltypes = types_helper::get_lti_types_by_course($course->id, $teacher->id); $this->assertCount(0, $coursetooltypes); } /** * Test fetching tool types for a given course and user. * * @covers ::override_type_showinactivitychooser * @return void. */ public function test_override_type_showinactivitychooser(): void { $this->resetAfterTest(); global $DB; $coursecat1 = $this->getDataGenerator()->create_category(); $coursecat2 = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]); $course2 = $this->getDataGenerator()->create_course(['category' => $coursecat2->id]); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); $context = \core\context\course::instance($course->id); $this->setUser($teacher); /* Create the following tool types for testing: | tooltype | coursevisible | restrictedtocategory | | site | LTI_COURSEVISIBLE_NO | | | site | LTI_COURSEVISIBLE_PRECONFIGURED | | | site | LTI_COURSEVISIBLE_ACTIVITYCHOOSER | yes | | site | LTI_COURSEVISIBLE_ACTIVITYCHOOSER | yes | | course | LTI_COURSEVISIBLE_ACTIVITYCHOOSER | | */ /** @var \mod_lti_generator $ltigenerator */ $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $tool1id = $ltigenerator->create_tool_types([ 'name' => 'site tool do not show', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_NO, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $tool2id = $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured only', 'baseurl' => 'http://example.com/tool/2', 'coursevisible' => LTI_COURSEVISIBLE_PRECONFIGURED, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $tool3id = $ltigenerator->create_course_tool_types([ 'name' => 'course tool preconfigured and activity chooser', 'baseurl' => 'http://example.com/tool/3', 'course' => $course->id ]); $tool4id = $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 2', 'baseurl' => 'http://example.com/tool/4', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); $tool5id = $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 1', 'baseurl' => 'http://example.com/tool/5', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat1->id ]); // LTI_COURSEVISIBLE_NO can't be updated. $result = types_helper::override_type_showinactivitychooser($tool1id, $course->id, $context, true); $this->assertFalse($result); // Tool not exist. $result = types_helper::override_type_showinactivitychooser($tool5id + 1, $course->id, $context, false); $this->assertFalse($result); $result = types_helper::override_type_showinactivitychooser($tool2id, $course->id, $context, true); $this->assertTrue($result); $coursevisibleoverriden = $DB->get_field('lti_coursevisible', 'coursevisible', ['typeid' => $tool2id, 'courseid' => $course->id]); $this->assertEquals(LTI_COURSEVISIBLE_ACTIVITYCHOOSER, $coursevisibleoverriden); $result = types_helper::override_type_showinactivitychooser($tool3id, $course->id, $context, false); $this->assertTrue($result); $coursevisible = $DB->get_field('lti_types', 'coursevisible', ['id' => $tool3id]); $this->assertEquals(LTI_COURSEVISIBLE_PRECONFIGURED, $coursevisible); // Restricted category no allowed. $this->expectException('moodle_exception'); $this->expectExceptionMessage('You are not allowed to change this setting for this tool.'); types_helper::override_type_showinactivitychooser($tool4id, $course->id, $context, false); // Restricted category allowed. $result = types_helper::override_type_showinactivitychooser($tool5id, $course->id, $context, false); $this->assertTrue($result); $coursevisibleoverriden = $DB->get_field('lti_coursevisible', 'coursevisible', ['typeid' => $tool5id, 'courseid' => $course->id]); $this->assertEquals(LTI_COURSEVISIBLE_PRECONFIGURED, $coursevisibleoverriden); $this->setUser($teacher2); $this->expectException(\required_capability_exception::class); types_helper::override_type_showinactivitychooser($tool5id, $course->id, $context, false); } } tests/mod_lti_testcase.php000064400000005360152265520610011751 0ustar00. use core_external\external_api; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * Abstract base testcase for mod_lti unit tests. * * @package mod_lti * @author Andrew Madden * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class mod_lti_testcase extends externallib_advanced_testcase { /** * Generate a tool type. * * @param string $uniqueid Each tool type needs a different base url. Provide a unique string for every tool type created. * @param int|null $toolproxyid Optional proxy to associate with tool type. * @return stdClass A tool type. */ protected function generate_tool_type(string $uniqueid, ?int $toolproxyid = null): stdClass { // Create a tool type. $type = new stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool $uniqueid"; $type->description = "Example description $uniqueid"; $type->toolproxyid = $toolproxyid; $type->baseurl = $this->getExternalTestFileUrl("/test$uniqueid.html"); $type->coursevisible = LTI_COURSEVISIBLE_ACTIVITYCHOOSER; $config = new stdClass(); $config->lti_coursevisible = LTI_COURSEVISIBLE_ACTIVITYCHOOSER; $type->id = lti_add_type($type, $config); return $type; } /** * Generate a tool proxy. * * @param string $uniqueid Each tool proxy needs a different reg url. Provide a unique string for every tool proxy created. * @return stdClass A tool proxy. */ protected function generate_tool_proxy(string $uniqueid): stdClass { // Create a tool proxy. $proxy = mod_lti_external::create_tool_proxy("Test proxy $uniqueid", $this->getExternalTestFileUrl("/proxy$uniqueid.html"), [], []); return (object)external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy); } } tests/servicelib_test.php000064400000006643152265520610011622 0ustar00. namespace mod_lti; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/lti/servicelib.php'); /** * Tests for servicelib.php * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class servicelib_test extends \basic_testcase { /** * Test that lti_parse_message_id never fails with good and bad XML. * * @dataProvider message_id_provider * @param mixed $expected Expected message ID. * @param string $xml XML to parse. */ public function test_lti_parse_message_id($expected, $xml): void { $xml = simplexml_load_string($xml); $this->assertEquals($expected, lti_parse_message_id($xml)); } /** * Test data provider for testing lti_parse_message_id * * @return array */ public static function message_id_provider(): array { $valid = << V1.0 9999 XML; $noheader = << V1.0 9999 XML; $noinfo = << V1.0 9999 XML; $noidentifier = << V1.0 XML; return array( array(9999, $valid), array('', $noheader), array('', $noinfo), array('', $noidentifier), ); } } tests/lib_test.php000064400000044007152265520610010235 0ustar00. /** * Unit tests for mod_lti lib * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ namespace mod_lti; defined('MOODLE_INTERNAL') || die(); /** * Unit tests for mod_lti lib * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ final class lib_test extends \advanced_testcase { /** * Prepares things before this test case is initialised * @return void */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/mod/lti/lib.php'); parent::setUpBeforeClass(); } /** * Test lti_view * @return void */ public function test_lti_view(): void { global $CFG; $CFG->enablecompletion = 1; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id), array('completion' => 2, 'completionview' => 1)); $context = \context_module::instance($lti->cmid); $cm = get_coursemodule_from_instance('lti', $lti->id); // Trigger and capture the event. $sink = $this->redirectEvents(); lti_view($lti, $course, $cm, $context); $events = $sink->get_events(); // 2 additional events thanks to completion. $this->assertCount(3, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_lti\event\course_module_viewed', $event); $this->assertEquals($context, $event->get_context()); $moodleurl = new \moodle_url('/mod/lti/view.php', array('id' => $cm->id)); $this->assertEquals($moodleurl, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); // Check completion status. $completion = new \completion_info($course); $completiondata = $completion->get_data($cm); $this->assertEquals(1, $completiondata->completionstate); } /** * Test deleting LTI instance. */ public function test_lti_delete_instance(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(array()); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); $cm = get_coursemodule_from_instance('lti', $lti->id); // Must not throw notices. course_delete_module($cm->id); } public function test_lti_core_calendar_provide_event_action(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_lti_core_calendar_provide_event_action_as_non_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_lti_core_calendar_provide_event_action_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); // Enrol a student in the course. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_lti_core_calendar_provide_event_action_already_completed(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('lti', $lti->id); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_lti_core_calendar_provide_event_action_already_completed_as_non_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('lti', $lti->id); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_lti_core_calendar_provide_event_action_already_completed_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Enrol 2 students in the course. $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Get some additional data. $cm = get_coursemodule_from_instance('lti', $lti->id); // Create a calendar event. $event = $this->create_action_event($course->id, $lti->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed for $student1. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student1->id); // Now, log in as $student2. $this->setUser($student2); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for $student1. $actionevent = mod_lti_core_calendar_provide_event_action($event, $factory, $student1->id); // Ensure result was null. $this->assertNull($actionevent); } /** * Creates an action event. * * @param int $courseid The course id. * @param int $instanceid The instance id. * @param string $eventtype The event type. * @return bool|calendar_event */ private function create_action_event($courseid, $instanceid, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'lti'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; $event->timestart = time(); return \calendar_event::create($event); } /** * Test verifying the output of the lti_get_course_content_items and lti_get_all_content_items callbacks. */ public function test_content_item_callbacks(): void { $this->resetAfterTest(); global $DB, $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); $admin = get_admin(); $time = time(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $course2 = $this->getDataGenerator()->create_course(); $teacher2 = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); // Create some preconfigured tools. $sitetoolrecord = (object) [ 'name' => 'Site level tool which is available in the activity chooser', 'baseurl' => 'http://example.com', 'createdby' => $admin->id, 'course' => SITEID, 'ltiversion' => 'LTI-1p0', 'timecreated' => $time, 'timemodified' => $time, 'state' => LTI_TOOL_STATE_CONFIGURED, 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER ]; $sitetoolrecordnonchooser = (object) [ 'name' => 'Site level tool which is NOT available in the course activity chooser', 'baseurl' => 'http://example2.com', 'createdby' => $admin->id, 'course' => SITEID, 'ltiversion' => 'LTI-1p0', 'timecreated' => $time, 'timemodified' => $time, 'state' => LTI_TOOL_STATE_CONFIGURED, 'coursevisible' => LTI_COURSEVISIBLE_PRECONFIGURED ]; $course1toolrecord = (object) [ 'name' => 'Course created tool which is available in the activity chooser', 'baseurl' => 'http://example3.com', 'createdby' => $teacher->id, 'course' => $course->id, 'ltiversion' => 'LTI-1p0', 'timecreated' => $time, 'timemodified' => $time, 'state' => LTI_TOOL_STATE_CONFIGURED, 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER ]; $course2toolrecord = (object) [ 'name' => 'Course created tool which is available in the activity chooser', 'baseurl' => 'http://example4.com', 'createdby' => $teacher2->id, 'course' => $course2->id, 'ltiversion' => 'LTI-1p0', 'timecreated' => $time, 'timemodified' => $time, 'state' => LTI_TOOL_STATE_CONFIGURED, 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER ]; $tool1id = $DB->insert_record('lti_types', $sitetoolrecord); $tool2id = $DB->insert_record('lti_types', $sitetoolrecordnonchooser); $tool3id = $DB->insert_record('lti_types', $course1toolrecord); $tool4id = $DB->insert_record('lti_types', $course2toolrecord); $sitetoolrecord->id = $tool1id; $sitetoolrecordnonchooser->id = $tool2id; $course1toolrecord->id = $tool3id; $course2toolrecord->id = $tool4id; $defaultmodulecontentitem = new \core_course\local\entity\content_item( '1', 'default module content item', new \core_course\local\entity\string_title('Content item title'), new \moodle_url(''), 'icon', 'Description of the module', MOD_ARCHETYPE_OTHER, 'mod_lti', MOD_PURPOSE_CONTENT ); // The lti_get_lti_types_by_course method (used by the callbacks) assumes the global user. $this->setUser($teacher); // Teacher in course1 should be able to see the site preconfigured tool and the tool created in course1. $courseitems = lti_get_course_content_items($defaultmodulecontentitem, $teacher, $course); $this->assertCount(2, $courseitems); $ids = []; foreach ($courseitems as $item) { $ids[] = $item->get_id(); } $this->assertContains($sitetoolrecord->id + 1, $ids); $this->assertContains($course1toolrecord->id + 1, $ids); $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids); // The content items for teacher2 in course2 include the site preconfigured tool and the tool created in course2. $this->setUser($teacher2); $course2items = lti_get_course_content_items($defaultmodulecontentitem, $teacher2, $course2); $this->assertCount(2, $course2items); $ids = []; foreach ($course2items as $item) { $ids[] = $item->get_id(); } $this->assertContains($sitetoolrecord->id + 1, $ids); $this->assertContains($course2toolrecord->id + 1, $ids); $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids); // Removing the capability to use preconfigured (site or course level) tools, should result in no content items. $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); assign_capability('mod/lti:addpreconfiguredinstance', CAP_PROHIBIT, $teacherrole->id, \core\context\course::instance($course2->id)); $course2items = lti_get_course_content_items($defaultmodulecontentitem, $teacher2, $course2); $this->assertCount(0, $course2items); // When fetching all content items, we expect to see all items available in activity choosers (in any course). $this->setAdminUser(); $allitems = mod_lti_get_all_content_items($defaultmodulecontentitem); $this->assertCount(3, $allitems); $ids = []; foreach ($allitems as $item) { $ids[] = $item->get_id(); } $this->assertContains($sitetoolrecord->id + 1, $ids); $this->assertContains($course1toolrecord->id + 1, $ids); $this->assertContains($course2toolrecord->id + 1, $ids); $this->assertNotContains($sitetoolrecordnonchooser->id + 1, $ids); } } tests/course_categories_trait.php000064400000004353152265520610013340 0ustar00. /** * Helpers for restrict course categories unit tests. * * @package mod_lti * @copyright 2023 Jackson D'Souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 */ trait mod_lti_course_categories_trait { /** * Setup course categories. * * @return array */ public function setup_course_categories(): array { global $DB; $topcatdbrecord = $DB->get_record('course_categories', ['parent' => 0]); $subcata = $this->getDataGenerator()->create_category(['parent' => $topcatdbrecord->id, 'name' => 'cata']); $subcatadbrecord = $DB->get_record('course_categories', ['id' => $subcata->id]); $subcatca = $this->getDataGenerator()->create_category(['parent' => $subcata->id, 'name' => 'catca']); $subcatcadbrecord = $DB->get_record('course_categories', ['id' => $subcatca->id]); $subcatb = $this->getDataGenerator()->create_category(['parent' => $topcatdbrecord->id, 'name' => 'catb']); $subcatbdbrecord = $DB->get_record('course_categories', ['id' => $subcatb->id]); $subcatcb = $this->getDataGenerator()->create_category(['parent' => $subcatb->id, 'name' => 'catcb']); $subcatcbdbrecord = $DB->get_record('course_categories', ['id' => $subcatcb->id]); return [ 'topcat' => $topcatdbrecord, 'subcata' => $subcatadbrecord, 'subcatca' => $subcatcadbrecord, 'subcatb' => $subcatb, 'subcatcb' => $subcatcbdbrecord ]; } } tests/locallib_test.php000064400000277170152265520610011261 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains unit tests for (some of) lti/locallib.php * * @package mod_lti * @category phpunit * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Charles Severance csev@unmich.edu * @author Marc Alier (marc.alier@upc.edu) * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti; use mod_lti_external; use mod_lti_testcase; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/servicelib.php'); require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * Local library tests * * @package mod_lti * @copyright Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class locallib_test extends mod_lti_testcase { /** * @covers ::lti_split_parameters() * * Test the split parameters function */ public function test_split_parameters(): void { $this->assertEquals(lti_split_parameters(''), array()); $this->assertEquals(lti_split_parameters('a=1'), array('a' => '1')); $this->assertEquals(lti_split_parameters("a=1\nb=2"), array('a' => '1', 'b' => '2')); $this->assertEquals(lti_split_parameters("a=1\n\rb=2"), array('a' => '1', 'b' => '2')); $this->assertEquals(lti_split_parameters("a=1\r\nb=2"), array('a' => '1', 'b' => '2')); } public function test_split_custom_parameters(): void { $this->resetAfterTest(); $tool = new \stdClass(); $tool->enabledcapability = ''; $tool->parameter = ''; $tool->ltiversion = 'LTI-1p0'; $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "x=1\ny=2", false), array('custom_x' => '1', 'custom_y' => '2')); // Check params with caps. $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), "X=1", true), array('custom_x' => '1', 'custom_X' => '1')); // Removed repeat of previous test with a semicolon separator. $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), 'Review:Chapter=1.2.56', true), array( 'custom_review_chapter' => '1.2.56', 'custom_Review:Chapter' => '1.2.56')); $this->assertEquals(lti_split_custom_parameters(null, $tool, array(), 'Complex!@#$^*(){}[]KEY=Complex!@#$^*;(){}[]½Value', true), array( 'custom_complex____________key' => 'Complex!@#$^*;(){}[]½Value', 'custom_Complex!@#$^*(){}[]KEY' => 'Complex!@#$^*;(){}[]½Value')); // Test custom parameter that returns $USER property. $user = $this->getDataGenerator()->create_user(array('middlename' => 'SOMETHING')); $this->setUser($user); $this->assertEquals(array('custom_x' => '1', 'custom_y' => 'SOMETHING'), lti_split_custom_parameters(null, $tool, array(), "x=1\ny=\$Person.name.middle", false)); } /** * This test has been disabled because the test-tool is * being moved and probably it won't work anymore for this. * We should be testing here local stuff only and leave * outside-checks to the conformance tests. MDL-30347 */ public function disabled_test_sign_parameters() { $correct = array ( 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing', 'ext_submit' => 'Click Me', 'lti_message_type' => 'basic-lti-launch-request', 'lti_version' => 'LTI-1p0', 'oauth_consumer_key' => 'lmsng.school.edu', 'oauth_nonce' => '47458148e33a8f9dafb888c3684cf476', 'oauth_signature' => 'qWgaBIezihCbeHgcwUy14tZcyDQ=', 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_timestamp' => '1307141660', 'oauth_version' => '1.0', 'resource_link_id' => '123', 'resource_link_title' => 'Weekly Blog', 'roles' => 'Learner', 'tool_consumer_instance_guid' => 'lmsng.school.edu', 'user_id' => '789'); $requestparams = array('resource_link_id' => '123', 'resource_link_title' => 'Weekly Blog', 'user_id' => '789', 'roles' => 'Learner', 'context_id' => '12345', 'context_label' => 'SI124', 'context_title' => 'Social Computing'); $parms = lti_sign_parameters($requestparams, 'http://www.imsglobal.org/developer/LTI/tool.php', 'POST', 'lmsng.school.edu', 'secret', 'Click Me', 'lmsng.school.edu' /*, $org_desc*/); $this->assertTrue(isset($parms['oauth_nonce'])); $this->assertTrue(isset($parms['oauth_signature'])); $this->assertTrue(isset($parms['oauth_timestamp'])); // Those things that are hard to mock. $correct['oauth_nonce'] = $parms['oauth_nonce']; $correct['oauth_signature'] = $parms['oauth_signature']; $correct['oauth_timestamp'] = $parms['oauth_timestamp']; ksort($parms); ksort($correct); $this->assertEquals($parms, $correct); } /** * This test has been disabled because, since its creation, * the sourceId generation has changed and surely this is outdated. * Some day these should be replaced by proper tests, but until then * conformance tests say this is working. MDL-30347 */ public function disabled_test_parse_grade_replace_message() { $message = ' V1.0 999998123 ' . '{"data":{"instanceid":"2","userid":"2"},"hash":' . '"0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b"}' . ' en-us 0.92 '; $parsed = lti_parse_grade_replace_message(new SimpleXMLElement($message)); $this->assertEquals($parsed->userid, '2'); $this->assertEquals($parsed->instanceid, '2'); $this->assertEquals($parsed->sourcedidhash, '0b5078feab59b9938c333ceaae21d8e003a7b295e43cdf55338445254421076b'); $ltiinstance = (object)array('servicesalt' => '4e5fcc06de1d58.44963230'); lti_verify_sourcedid($ltiinstance, $parsed); } public function test_lti_ensure_url_is_https(): void { $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('http://moodle.org')); $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('moodle.org')); $this->assertEquals('https://moodle.org', lti_ensure_url_is_https('https://moodle.org')); } /** * Test lti_get_url_thumbprint against various URLs */ public function test_lti_get_url_thumbprint(): void { // Note: trailing and double slash are expected right now. Must evaluate if it must be removed at some point. $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://MOODLE.ORG')); $this->assertEquals('moodle.org/', lti_get_url_thumbprint('http://www.moodle.org')); $this->assertEquals('moodle.org/', lti_get_url_thumbprint('https://www.moodle.org')); $this->assertEquals('moodle.org/', lti_get_url_thumbprint('moodle.org')); $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('http://moodle.org/this/is/moodle')); $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('https://moodle.org/this/is/moodle')); $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle')); $this->assertEquals('moodle.org//this/is/moodle', lti_get_url_thumbprint('moodle.org/this/is/moodle?')); $this->assertEquals('moodle.org//this/is/moodle?foo=bar', lti_get_url_thumbprint('moodle.org/this/is/moodle?foo=bar')); } /* * Verify that lti_build_request does handle resource_link_id as expected */ public function test_lti_buid_request_resource_link_id(): void { $this->resetAfterTest(); self::setUser($this->getDataGenerator()->create_user()); $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module('lti', array( 'intro' => "

This

\nhas\r\n

some

\nnew\n\rlines", 'introformat' => FORMAT_HTML, 'course' => $course->id, )); $typeconfig = array( 'acceptgrades' => 1, 'forcessl' => 0, 'sendname' => 2, 'sendemailaddr' => 2, 'customparameters' => '', ); // Normal call, we expect $instance->id to be used as resource_link_id. $params = lti_build_request($instance, $typeconfig, $course, null); $this->assertSame($instance->id, $params['resource_link_id']); // If there is a resource_link_id set, it gets precedence. $instance->resource_link_id = $instance->id + 99; $params = lti_build_request($instance, $typeconfig, $course, null); $this->assertSame($instance->resource_link_id, $params['resource_link_id']); // With none set, resource_link_id is not set either. unset($instance->id); unset($instance->resource_link_id); $params = lti_build_request($instance, $typeconfig, $course, null); $this->assertArrayNotHasKey('resource_link_id', $params); } /** * Test lti_build_request's resource_link_description and ensure * that the newlines in the description are correct. */ public function test_lti_build_request_description(): void { $this->resetAfterTest(); self::setUser($this->getDataGenerator()->create_user()); $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module('lti', array( 'intro' => "

This

\nhas\r\n

some

\nnew\n\rlines", 'introformat' => FORMAT_HTML, 'course' => $course->id, )); $typeconfig = array( 'acceptgrades' => 1, 'forcessl' => 0, 'sendname' => 2, 'sendemailaddr' => 2, 'customparameters' => '', ); $params = lti_build_request($instance, $typeconfig, $course, null); $ncount = substr_count($params['resource_link_description'], "\n"); $this->assertGreaterThan(0, $ncount); $rcount = substr_count($params['resource_link_description'], "\r"); $this->assertGreaterThan(0, $rcount); $this->assertEquals($ncount, $rcount, 'The number of \n characters should be the same as the number of \r characters'); $rncount = substr_count($params['resource_link_description'], "\r\n"); $this->assertGreaterThan(0, $rncount); $this->assertEquals($ncount, $rncount, 'All newline characters should be a combination of \r\n'); } /** * Tests lti_prepare_type_for_save's handling of the "Force SSL" configuration. */ public function test_lti_prepare_type_for_save_forcessl(): void { $type = new \stdClass(); $config = new \stdClass(); // Try when the forcessl config property is not set. lti_prepare_type_for_save($type, $config); $this->assertObjectHasProperty('lti_forcessl', $config); $this->assertEquals(0, $config->lti_forcessl); $this->assertEquals(0, $type->forcessl); // Try when forcessl config property is set. $config->lti_forcessl = 1; lti_prepare_type_for_save($type, $config); $this->assertObjectHasProperty('lti_forcessl', $config); $this->assertEquals(1, $config->lti_forcessl); $this->assertEquals(1, $type->forcessl); // Try when forcessl config property is set to 0. $config->lti_forcessl = 0; lti_prepare_type_for_save($type, $config); $this->assertObjectHasProperty('lti_forcessl', $config); $this->assertEquals(0, $config->lti_forcessl); $this->assertEquals(0, $type->forcessl); } /** * Tests lti_load_type_from_cartridge and lti_load_type_if_cartridge */ public function test_lti_load_type_from_cartridge(): void { $type = new \stdClass(); $type->lti_toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'); lti_load_type_if_cartridge($type); $this->assertEquals('Example tool', $type->lti_typename); $this->assertEquals('Example tool description', $type->lti_description); $this->assertEquals('http://www.example.com/lti/provider.php', $type->lti_toolurl); $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $type->lti_icon); $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $type->lti_secureicon); } /** * Tests lti_load_tool_from_cartridge and lti_load_tool_if_cartridge */ public function test_lti_load_tool_from_cartridge(): void { $lti = new \stdClass(); $lti->toolurl = $this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'); lti_load_tool_if_cartridge($lti); $this->assertEquals('Example tool', $lti->name); $this->assertEquals('Example tool description', $lti->intro); $this->assertEquals('http://www.example.com/lti/provider.php', $lti->toolurl); $this->assertEquals('https://www.example.com/lti/provider.php', $lti->securetoolurl); $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $lti->icon); $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $lti->secureicon); } /** * Tests for lti_build_content_item_selection_request(). */ public function test_lti_build_content_item_selection_request(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool proxy. $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); // Create a tool type, associated with that proxy. $type = new \stdClass(); $data = new \stdClass(); $data->lti_contentitem = true; $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->toolproxyid = $proxy->id; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $typeid = lti_add_type($type, $data); $typeconfig = lti_get_type_config($typeid); $course = $this->getDataGenerator()->create_course(); $returnurl = new \moodle_url('/'); // Default parameters. $result = lti_build_content_item_selection_request($typeid, $course, $returnurl); $this->assertNotEmpty($result); $this->assertNotEmpty($result->params); $this->assertNotEmpty($result->url); $params = $result->params; $url = $result->url; $this->assertEquals($typeconfig['toolurl'], $url); $this->assertEquals('ContentItemSelectionRequest', $params['lti_message_type']); $this->assertEquals(LTI_VERSION_1, $params['lti_version']); $this->assertEquals('application/vnd.ims.lti.v1.ltilink', $params['accept_media_types']); $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']); $this->assertEquals($returnurl->out(false), $params['content_item_return_url']); $this->assertEquals('false', $params['accept_unsigned']); $this->assertEquals('true', $params['accept_multiple']); $this->assertEquals('false', $params['accept_copy_advice']); $this->assertEquals('false', $params['auto_create']); $this->assertEquals($type->name, $params['title']); $this->assertFalse(isset($params['resource_link_id'])); $this->assertFalse(isset($params['resource_link_title'])); $this->assertFalse(isset($params['resource_link_description'])); $this->assertFalse(isset($params['launch_presentation_return_url'])); $this->assertFalse(isset($params['lis_result_sourcedid'])); $this->assertEquals($params['tool_consumer_instance_guid'], 'www.example.com'); // Custom parameters. $title = 'My custom title'; $text = 'This is the tool description'; $mediatypes = ['image/*', 'video/*']; $targets = ['embed', 'iframe']; $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets, true, true, true, true, true); $this->assertNotEmpty($result); $this->assertNotEmpty($result->params); $this->assertNotEmpty($result->url); $params = $result->params; $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']); $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']); $this->assertEquals('true', $params['accept_unsigned']); $this->assertEquals('true', $params['accept_multiple']); $this->assertEquals('true', $params['accept_copy_advice']); $this->assertEquals('true', $params['auto_create']); $this->assertEquals($title, $params['title']); $this->assertEquals($text, $params['text']); // Invalid flag values. $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets, 'aa', -1, 0, 1, 0xabc); $this->assertNotEmpty($result); $this->assertNotEmpty($result->params); $this->assertNotEmpty($result->url); $params = $result->params; $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']); $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']); $this->assertEquals('false', $params['accept_unsigned']); $this->assertEquals('false', $params['accept_multiple']); $this->assertEquals('false', $params['accept_copy_advice']); $this->assertEquals('false', $params['auto_create']); $this->assertEquals($title, $params['title']); $this->assertEquals($text, $params['text']); } /** * Test for lti_build_content_item_selection_request() with nonexistent tool type ID parameter. */ public function test_lti_build_content_item_selection_request_invalid_tooltype(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $returnurl = new \moodle_url('/'); // Should throw Exception on non-existent tool type. $this->expectException('moodle_exception'); lti_build_content_item_selection_request(1, $course, $returnurl); } /** * Test for lti_build_content_item_selection_request() with invalid media types parameter. */ public function test_lti_build_content_item_selection_request_invalid_mediatypes(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $data = new \stdClass(); $data->lti_contentitem = true; $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $typeid = lti_add_type($type, $data); $course = $this->getDataGenerator()->create_course(); $returnurl = new \moodle_url('/'); // Should throw coding_exception on non-array media types. $mediatypes = 'image/*,video/*'; $this->expectException('coding_exception'); lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', $mediatypes); } /** * Test for lti_build_content_item_selection_request() with invalid presentation targets parameter. */ public function test_lti_build_content_item_selection_request_invalid_presentationtargets(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $data = new \stdClass(); $data->lti_contentitem = true; $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $typeid = lti_add_type($type, $data); $course = $this->getDataGenerator()->create_course(); $returnurl = new \moodle_url('/'); // Should throw coding_exception on non-array presentation targets. $targets = 'frame,iframe'; $this->expectException('coding_exception'); lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', [], $targets); } /** * Provider for test_lti_get_best_tool_by_url. * * @return array of [urlToTest, expectedTool, allTools] */ public static function lti_get_best_tool_by_url_provider(): array { $tools = [ (object) [ 'name' => 'Here', 'baseurl' => 'https://example.com/i/am/?where=here', 'tooldomain' => 'example.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], (object) [ 'name' => 'There', 'baseurl' => 'https://example.com/i/am/?where=there', 'tooldomain' => 'example.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], (object) [ 'name' => 'Not here', 'baseurl' => 'https://example.com/i/am/?where=not/here', 'tooldomain' => 'example.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], (object) [ 'name' => 'Here', 'baseurl' => 'https://example.com/i/am/', 'tooldomain' => 'example.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], (object) [ 'name' => 'Here', 'baseurl' => 'https://example.com/i/was', 'tooldomain' => 'example.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], (object) [ 'name' => 'Here', 'baseurl' => 'https://badexample.com/i/am/?where=here', 'tooldomain' => 'badexample.com', 'state' => LTI_TOOL_STATE_CONFIGURED, 'course' => SITEID ], ]; $data = [ [ 'url' => $tools[0]->baseurl, 'expected' => $tools[0], ], [ 'url' => $tools[1]->baseurl, 'expected' => $tools[1], ], [ 'url' => $tools[2]->baseurl, 'expected' => $tools[2], ], [ 'url' => $tools[3]->baseurl, 'expected' => $tools[3], ], [ 'url' => $tools[4]->baseurl, 'expected' => $tools[4], ], [ 'url' => $tools[5]->baseurl, 'expected' => $tools[5], ], [ 'url' => 'https://nomatch.com/i/am/', 'expected' => null ], [ 'url' => 'https://example.com', 'expected' => null ], [ 'url' => 'https://example.com/i/am/?where=unknown', 'expected' => $tools[3] ] ]; // Construct the final array as required by the provider API. Each row // of the array contains the URL to test, the expected tool, and // the complete list of tools. return array_map(function($data) use ($tools): array { return [$data['url'], $data['expected'], $tools]; }, $data); } /** * Test lti_get_best_tool_by_url. * * @dataProvider lti_get_best_tool_by_url_provider * @param string $url The URL to test. * @param object $expected The expected tool matching the URL. * @param array $tools The pool of tools to match the URL with. */ public function test_lti_get_best_tool_by_url($url, $expected, $tools): void { $actual = lti_get_best_tool_by_url($url, $tools, null); $this->assertSame($expected, $actual); } /** * @covers ::lti_get_tools_by_domain() * * Test lti_get_tools_by_domain. */ public function test_lti_get_tools_by_domain(): void { $this->resetAfterTest(); /** @var \mod_lti_generator $ltigenerator */ $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); // Create a tool type with good domain. $ltigenerator->create_tool_types([ 'name' => 'Test tool 1', 'description' => 'Good example description', 'tooldomain' => 'example.com', 'baseurl' => 'https://example.com/i/am/?where=here', 'state' => LTI_TOOL_STATE_CONFIGURED ]); // Create a tool type with bad domain. $ltigenerator->create_tool_types([ 'name' => 'Test tool 2', 'description' => 'Bad example description', 'tooldomain' => 'badexample.com', 'baseurl' => 'https://badexample.com/i/am/?where=here', 'state' => LTI_TOOL_STATE_CONFIGURED ]); $records = lti_get_tools_by_domain('example.com', LTI_TOOL_STATE_CONFIGURED); $this->assertCount(1, $records); $this->assertEmpty(array_diff( ['https://example.com/i/am/?where=here'], array_column($records, 'baseurl') )); } /** * @covers ::lti_get_tools_by_domain() * * Test test_lti_get_tools_by_domain_restrict_types_category. */ public function test_lti_get_tools_by_domain_restrict_types_category(): void { $this->resetAfterTest(); $coursecat1 = $this->getDataGenerator()->create_category(); $coursecat2 = $this->getDataGenerator()->create_category(); $course1 = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]); $course2 = $this->getDataGenerator()->create_course(['category' => $coursecat2->id]); /** @var \mod_lti_generator $ltigenerator */ $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); // Create a tool type with domain restricting to a category1. $ltigenerator->create_tool_types([ 'name' => 'Test tool 1', 'description' => 'Good example description', 'tooldomain' => 'exampleone.com', 'baseurl' => 'https://exampleone.com/tool/1', 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat1->id ]); // Create another tool type using the same domain, restricted to category2. $ltigenerator->create_tool_types([ 'name' => 'Test tool 1', 'description' => 'Good example description', 'tooldomain' => 'exampleone.com', 'baseurl' => 'https://exampleone.com/tool/2', 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); // Create a tool type with domain restricting to a category2. $ltigenerator->create_tool_types([ 'name' => 'Test tool 2', 'description' => 'Good example description', 'tooldomain' => 'exampletwo.com', 'baseurl' => 'https://exampletwo.com/tool/3', 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); // Get tool types for domain 'exampleone' in course 1 and verify only the one result under course category 1 is included. $records = lti_get_tools_by_domain('exampleone.com', LTI_TOOL_STATE_CONFIGURED, $course1->id); $this->assertCount(1, $records); $this->assertEmpty(array_diff( ['https://exampleone.com/tool/1'], array_column($records, 'baseurl') )); // Get tool types for domain 'exampleone' in course 2 and verify only the one result under course category 2 is included. $records = lti_get_tools_by_domain('exampleone.com', LTI_TOOL_STATE_CONFIGURED, $course2->id); $this->assertCount(1, $records); $this->assertEmpty(array_diff( ['https://exampleone.com/tool/2'], array_column($records, 'baseurl') )); // Get tool types for domain 'exampletwo' in course 1 and verify that no results are found. $records = lti_get_tools_by_domain('exampletwo.com', LTI_TOOL_STATE_CONFIGURED, $course1->id); $this->assertCount(0, $records); } /** * Test lti_get_jwt_message_type_mapping(). */ public function test_lti_get_jwt_message_type_mapping(): void { $mapping = [ 'basic-lti-launch-request' => 'LtiResourceLinkRequest', 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest', 'LtiDeepLinkingResponse' => 'ContentItemSelection', 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest' ]; $this->assertEquals($mapping, lti_get_jwt_message_type_mapping()); } /** * Test lti_get_jwt_claim_mapping() */ public function test_lti_get_jwt_claim_mapping(): void { $mapping = [ 'accept_copy_advice' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_copy_advice', 'isarray' => false, 'type' => 'boolean' ], 'accept_media_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_media_types', 'isarray' => true ], 'accept_multiple' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_multiple', 'isarray' => false, 'type' => 'boolean' ], 'accept_presentation_document_targets' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_presentation_document_targets', 'isarray' => true ], 'accept_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_types', 'isarray' => true ], 'accept_unsigned' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_unsigned', 'isarray' => false, 'type' => 'boolean' ], 'auto_create' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'auto_create', 'isarray' => false, 'type' => 'boolean' ], 'can_confirm' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'can_confirm', 'isarray' => false, 'type' => 'boolean' ], 'content_item_return_url' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'deep_link_return_url', 'isarray' => false ], 'content_items' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isarray' => true ], 'data' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'data', 'isarray' => false ], 'text' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'text', 'isarray' => false ], 'title' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'title', 'isarray' => false ], 'lti_msg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'msg', 'isarray' => false ], 'lti_log' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'log', 'isarray' => false ], 'lti_errormsg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errormsg', 'isarray' => false ], 'lti_errorlog' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errorlog', 'isarray' => false ], 'context_id' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'id', 'isarray' => false ], 'context_label' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'label', 'isarray' => false ], 'context_title' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'title', 'isarray' => false ], 'context_type' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'type', 'isarray' => true ], 'lis_course_offering_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_offering_sourcedid', 'isarray' => false ], 'lis_course_section_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_section_sourcedid', 'isarray' => false ], 'launch_presentation_css_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'css_url', 'isarray' => false ], 'launch_presentation_document_target' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'document_target', 'isarray' => false ], 'launch_presentation_height' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'height', 'isarray' => false ], 'launch_presentation_locale' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'locale', 'isarray' => false ], 'launch_presentation_return_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'return_url', 'isarray' => false ], 'launch_presentation_width' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'width', 'isarray' => false ], 'lis_person_contact_email_primary' => [ 'suffix' => '', 'group' => null, 'claim' => 'email', 'isarray' => false ], 'lis_person_name_family' => [ 'suffix' => '', 'group' => null, 'claim' => 'family_name', 'isarray' => false ], 'lis_person_name_full' => [ 'suffix' => '', 'group' => null, 'claim' => 'name', 'isarray' => false ], 'lis_person_name_given' => [ 'suffix' => '', 'group' => null, 'claim' => 'given_name', 'isarray' => false ], 'lis_person_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'person_sourcedid', 'isarray' => false ], 'user_id' => [ 'suffix' => '', 'group' => null, 'claim' => 'sub', 'isarray' => false ], 'user_image' => [ 'suffix' => '', 'group' => null, 'claim' => 'picture', 'isarray' => false ], 'roles' => [ 'suffix' => '', 'group' => '', 'claim' => 'roles', 'isarray' => true ], 'role_scope_mentor' => [ 'suffix' => '', 'group' => '', 'claim' => 'role_scope_mentor', 'isarray' => false ], 'deployment_id' => [ 'suffix' => '', 'group' => '', 'claim' => 'deployment_id', 'isarray' => false ], 'lti_message_type' => [ 'suffix' => '', 'group' => '', 'claim' => 'message_type', 'isarray' => false ], 'lti_version' => [ 'suffix' => '', 'group' => '', 'claim' => 'version', 'isarray' => false ], 'resource_link_description' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'description', 'isarray' => false ], 'resource_link_id' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'id', 'isarray' => false ], 'resource_link_title' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'title', 'isarray' => false ], 'tool_consumer_info_product_family_code' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'product_family_code', 'isarray' => false ], 'tool_consumer_info_version' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'version', 'isarray' => false ], 'tool_consumer_instance_contact_email' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'contact_email', 'isarray' => false ], 'tool_consumer_instance_description' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'description', 'isarray' => false ], 'tool_consumer_instance_guid' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'guid', 'isarray' => false ], 'tool_consumer_instance_name' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'name', 'isarray' => false ], 'tool_consumer_instance_url' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'url', 'isarray' => false ], 'custom_context_memberships_v2_url' => [ 'suffix' => 'nrps', 'group' => 'namesroleservice', 'claim' => 'context_memberships_url', 'isarray' => false ], 'custom_context_memberships_versions' => [ 'suffix' => 'nrps', 'group' => 'namesroleservice', 'claim' => 'service_versions', 'isarray' => true ], 'custom_gradebookservices_scope' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scope', 'isarray' => true ], 'custom_lineitems_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitems', 'isarray' => false ], 'custom_lineitem_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitem', 'isarray' => false ], 'custom_results_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'results', 'isarray' => false ], 'custom_result_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'result', 'isarray' => false ], 'custom_scores_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scores', 'isarray' => false ], 'custom_score_url' => [ 'suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'score', 'isarray' => false ], 'lis_outcome_service_url' => [ 'suffix' => 'bo', 'group' => 'basicoutcome', 'claim' => 'lis_outcome_service_url', 'isarray' => false ], 'lis_result_sourcedid' => [ 'suffix' => 'bo', 'group' => 'basicoutcome', 'claim' => 'lis_result_sourcedid', 'isarray' => false ], 'for_user_id' => [ 'suffix' => '', 'group' => 'for_user', 'claim' => 'user_id', 'isarray' => false ], ]; $actual = lti_get_jwt_claim_mapping(); $this->assertEquals($mapping, $actual); } /** * Test lti_build_standard_message(). */ public function test_lti_build_standard_message_institution_name_set(): void { global $CFG; $this->resetAfterTest(); $CFG->mod_lti_institution_name = 'some institution name lols'; $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module('lti', [ 'course' => $course->id, ] ); $message = lti_build_standard_message($instance, '2', LTI_VERSION_1); $this->assertEquals('moodle-2', $message['ext_lms']); $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']); $this->assertEquals(LTI_VERSION_1, $message['lti_version']); $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']); $this->assertEquals('2', $message['tool_consumer_instance_guid']); $this->assertEquals('some institution name lols', $message['tool_consumer_instance_name']); $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']); } /** * Test lti_build_standard_message(). */ public function test_lti_build_standard_message_institution_name_not_set(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module('lti', [ 'course' => $course->id, ] ); $message = lti_build_standard_message($instance, '2', LTI_VERSION_2); $this->assertEquals('moodle-2', $message['ext_lms']); $this->assertEquals('moodle', $message['tool_consumer_info_product_family_code']); $this->assertEquals(LTI_VERSION_2, $message['lti_version']); $this->assertEquals('basic-lti-launch-request', $message['lti_message_type']); $this->assertEquals('2', $message['tool_consumer_instance_guid']); $this->assertEquals('phpunit', $message['tool_consumer_instance_name']); $this->assertEquals('PHPUnit test site', $message['tool_consumer_instance_description']); } /** * Test lti_verify_jwt_signature(). */ public function test_lti_verify_jwt_signature(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->lti_publickey = '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----'; $config->lti_keytype = LTI_RSA_KEY; $typeid = lti_add_type($type, $config); lti_verify_jwt_signature($typeid, '', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g' . 'RG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOs' . 'S_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMgu' . 'EIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iY' . 'v7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA'); } /** * Test lti_verify_jwt_signature_jwk(). */ public function test_lti_verify_jwt_signature_jwk(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->lti_publickeyset = $this->getExternalTestFileUrl('/lti_keyset.json'); $config->lti_keytype = LTI_JWK_KEYSET; $typeid = lti_add_type($type, $config); $jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjU3YzExNzdkMmQ1M2EwMjFjNzM'; $jwt .= '3NTY0OTFjMTM3YjE3In0.eyJpc3MiOiJnclJvbkd3RTd1WjRwZ28iLCJzdWIiOiJnclJvb'; $jwt .= 'kd3RTd1WjRwZ28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0L21vb2RsZS9tb2QvbHRpL3R'; $jwt .= 'va2VuLnBocCIsImp0aSI6IjFlMUJPVEczVFJjbFdUem00dERsMGc9PSIsImlhdCI6MTU4M'; $jwt .= 'Dg1NTUwNX0.Lowhc9ovNAXRb2rkAnv1oozDXlRD54Mz2JS1i8Zx4yGWQzmXzam-La19_g0'; $jwt .= 'CTnwlKM6gxaInnRKFRAcwhJVcWec389liLAjMbna6d6iTWYTZr7q_4BIe3CT_oTMWASGta'; $jwt .= 'Paaq53ch1rO4YdueEtmtd1K47ibo4Lhu1jmP_icc3lxjfnqiv4vIYdy7W2JQEzpk1ImuQr'; $jwt .= 'AlO1xR3fZ6bgcJhVIaw5xoaZD3ZgEjuZOQXMkywv1bL-mL17RX336CzHd8rYZg82QXrBzb'; $jwt .= 'NWzAlaZxv9VSug8t6mORvM6TkYYWjqEBKemgkD5rNh1BHrPcjWP7vy2Jz7YMjLsmuvDuLK'; $jwt .= '_PHYIKL--s4gcXWoYmOu1vj-SgoPczTJPoiBD35hAKqVHy5ggHaYHBy95_bbcFd8H1smHw'; $jwt .= 'pejrAFj1QAwGyTISLzUm08oq7Ak0tSxRKKXw4lpZAka1MmYxO3tJ_3-MXw6Bwz12bNgitJ'; $jwt .= 'lQd6n3kkGLCJAmANeRkPsH6eZVwF0n2cjh2O1JAwyNcMD2vs4I8ftM1EqqoE2M3r6kt3AC'; $jwt .= 'EscmqzizI3j80USBCLUUb1UTsfJb2g7oyApJAp-13Q3InR3QyvWO8unG5VraFE7IL5I28h'; $jwt .= 'MkQAHuCI90DFmXB4leflAu7wNlIK_U8xkGl8X8Mnv6MWgg94Ki8jgIq_kA85JAqI'; lti_verify_jwt_signature($typeid, '', $jwt); } /** * Test lti_verify_jwt_signature(). */ public function test_lti_verify_jwt_signature_with_lti2(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool proxy. $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->toolproxyid = $proxy->id; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $data = new \stdClass(); $data->lti_contentitem = true; $typeid = lti_add_type($type, $data); $this->expectExceptionMessage('JWT security not supported with LTI 2'); lti_verify_jwt_signature($typeid, '', ''); } /** * Test lti_verify_jwt_signature(). */ public function test_lti_verify_jwt_signature_no_consumer_key(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = 'consumerkey'; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $typeid = lti_add_type($type, $config); $this->expectExceptionMessage(get_string('errorincorrectconsumerkey', 'mod_lti')); lti_verify_jwt_signature($typeid, '', ''); } /** * Test lti_verify_jwt_signature(). */ public function test_lti_verify_jwt_signature_no_public_key(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = 'consumerkey'; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->lti_keytype = LTI_RSA_KEY; $typeid = lti_add_type($type, $config); $this->expectExceptionMessage('No public key configured'); lti_verify_jwt_signature($typeid, 'consumerkey', ''); } /** * Test lti_convert_content_items(). */ public function test_lti_convert_content_items(): void { $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'text' => 'Test text', 'iframe' => [] ]; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch2', 'title' => 'Test title2', 'text' => 'Test text2', 'iframe' => [ 'height' => 200, 'width' => 300 ], 'window' => [] ]; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch3', 'title' => 'Test title3', 'text' => 'Test text3', 'window' => [ 'targetName' => 'test-win', 'height' => 400 ] ]; $contentitems = json_encode($contentitems); $json = lti_convert_content_items($contentitems); $jsondecode = json_decode($json); $strcontext = '@context'; $strgraph = '@graph'; $strtype = '@type'; $objgraph = new \stdClass(); $objgraph->url = 'http://example.com/messages/launch'; $objgraph->title = 'Test title'; $objgraph->text = 'Test text'; $objgraph->placementAdvice = new \stdClass(); $objgraph->placementAdvice->presentationDocumentTarget = 'iframe'; $objgraph->{$strtype} = 'LtiLinkItem'; $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; $objgraph2 = new \stdClass(); $objgraph2->url = 'http://example.com/messages/launch2'; $objgraph2->title = 'Test title2'; $objgraph2->text = 'Test text2'; $objgraph2->placementAdvice = new \stdClass(); $objgraph2->placementAdvice->presentationDocumentTarget = 'iframe'; $objgraph2->placementAdvice->displayHeight = 200; $objgraph2->placementAdvice->displayWidth = 300; $objgraph2->{$strtype} = 'LtiLinkItem'; $objgraph2->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; $objgraph3 = new \stdClass(); $objgraph3->url = 'http://example.com/messages/launch3'; $objgraph3->title = 'Test title3'; $objgraph3->text = 'Test text3'; $objgraph3->placementAdvice = new \stdClass(); $objgraph3->placementAdvice->presentationDocumentTarget = 'window'; $objgraph3->placementAdvice->displayHeight = 400; $objgraph3->placementAdvice->windowTarget = 'test-win'; $objgraph3->{$strtype} = 'LtiLinkItem'; $objgraph3->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; $expected = new \stdClass(); $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; $expected->{$strgraph} = []; $expected->{$strgraph}[] = $objgraph; $expected->{$strgraph}[] = $objgraph2; $expected->{$strgraph}[] = $objgraph3; $this->assertEquals($expected, $jsondecode); } /** * Test adding a single gradable item through content item. */ public function test_lti_tool_configuration_from_content_item_single_gradable(): void { $this->resetAfterTest(); $this->setAdminUser(); $type = new \stdClass(); $type->name = "Test tool"; $type->baseurl = "http://example.com"; $config = new \stdClass(); $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $typeid = lti_add_type($type, $config); $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'lineItem' => [ 'resourceId' => 'r12345', 'tag' => 'final', 'scoreMaximum' => 10.0 ], 'frame' => [] ]; $contentitemsjson13 = json_encode($contentitems); $json11 = lti_convert_content_items($contentitemsjson13); $config = lti_tool_configuration_from_content_item($typeid, 'ContentItemSelection', $type->ltiversion, 'ConsumerKey', $json11); $this->assertEquals($contentitems[0]['url'], $config->toolurl); $this->assertEquals(LTI_SETTING_ALWAYS, $config->instructorchoiceacceptgrades); $this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag); $this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid); $this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point); $this->assertEquals('', $config->lineitemsubreviewurl); $this->assertEquals('', $config->lineitemsubreviewparams); } /** * @covers ::lti_tool_configuration_from_content_item() * * Test adding a single gradable item through content item with an empty subreview url. */ public function test_lti_tool_configuration_from_content_item_single_gradable_subreview_default_emptyurl(): void { $this->resetAfterTest(); $this->setAdminUser(); $type = new \stdClass(); $type->name = "Test tool"; $type->baseurl = "http://example.com"; $config = new \stdClass(); $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $typeid = lti_add_type($type, $config); $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'lineItem' => [ 'resourceId' => 'r12345', 'tag' => 'final', 'scoreMaximum' => 10.0, 'submissionReview' => [ 'url' => '' ] ], 'frame' => [] ]; $contentitemsjson13 = json_encode($contentitems); $json11 = lti_convert_content_items($contentitemsjson13); $config = lti_tool_configuration_from_content_item($typeid, 'ContentItemSelection', $type->ltiversion, 'ConsumerKey', $json11); $this->assertEquals('DEFAULT', $config->lineitemsubreviewurl); $this->assertEquals('', $config->lineitemsubreviewparams); } /** * @covers ::lti_tool_configuration_from_content_item() * * Test adding a single gradable item through content item. */ public function test_lti_tool_configuration_from_content_item_single_gradable_subreview_default(): void { $this->resetAfterTest(); $this->setAdminUser(); $type = new \stdClass(); $type->name = "Test tool"; $type->baseurl = "http://example.com"; $config = new \stdClass(); $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $typeid = lti_add_type($type, $config); $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'lineItem' => [ 'resourceId' => 'r12345', 'tag' => 'final', 'scoreMaximum' => 10.0, 'submissionReview' => [] ], 'frame' => [] ]; $contentitemsjson13 = json_encode($contentitems); $json11 = lti_convert_content_items($contentitemsjson13); $config = lti_tool_configuration_from_content_item($typeid, 'ContentItemSelection', $type->ltiversion, 'ConsumerKey', $json11); $this->assertEquals($contentitems[0]['url'], $config->toolurl); $this->assertEquals(LTI_SETTING_ALWAYS, $config->instructorchoiceacceptgrades); $this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag); $this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid); $this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point); $this->assertEquals('DEFAULT', $config->lineitemsubreviewurl); $this->assertEquals('', $config->lineitemsubreviewparams); } /** * Test adding multiple gradable items through content item. */ public function test_lti_tool_configuration_from_content_item_multiple(): void { $this->resetAfterTest(); $this->setAdminUser(); $type = new \stdClass(); $type->name = "Test tool"; $type->baseurl = "http://example.com"; $config = new \stdClass(); $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $typeid = lti_add_type($type, $config); $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'text' => 'Test text', 'icon' => [ 'url' => 'http://lti.example.com/image.jpg', 'width' => 100 ], 'frame' => [] ]; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launchgraded', 'title' => 'Test Graded', 'lineItem' => [ 'resourceId' => 'r12345', 'tag' => 'final', 'scoreMaximum' => 10.0, 'submissionReview' => [ 'url' => 'https://testsub.url', 'custom' => ['a' => 'b'] ] ], 'frame' => [] ]; $contentitemsjson13 = json_encode($contentitems); $json11 = lti_convert_content_items($contentitemsjson13); $config = lti_tool_configuration_from_content_item($typeid, 'ContentItemSelection', $type->ltiversion, 'ConsumerKey', $json11); $this->assertNotNull($config->multiple); $this->assertEquals(2, count( $config->multiple )); $this->assertEquals($contentitems[0]['title'], $config->multiple[0]->name); $this->assertEquals($contentitems[0]['url'], $config->multiple[0]->toolurl); $this->assertEquals(LTI_SETTING_NEVER, $config->multiple[0]->instructorchoiceacceptgrades); $this->assertEquals($contentitems[1]['url'], $config->multiple[1]->toolurl); $this->assertEquals(LTI_SETTING_ALWAYS, $config->multiple[1]->instructorchoiceacceptgrades); $this->assertEquals($contentitems[1]['lineItem']['tag'], $config->multiple[1]->lineitemtag); $this->assertEquals($contentitems[1]['lineItem']['resourceId'], $config->multiple[1]->lineitemresourceid); $this->assertEquals($contentitems[1]['lineItem']['scoreMaximum'], $config->multiple[1]->grade_modgrade_point); $this->assertEquals($contentitems[1]['lineItem']['submissionReview']['url'], $config->multiple[1]->lineitemsubreviewurl); $this->assertEquals("a=b", $config->multiple[1]->lineitemsubreviewparams); } /** * Test adding a single non gradable item through content item. */ public function test_lti_tool_configuration_from_content_item_single(): void { $this->resetAfterTest(); $this->setAdminUser(); $type = new \stdClass(); $type->name = "Test tool"; $type->baseurl = "http://example.com"; $config = new \stdClass(); $typeid = lti_add_type($type, $config); $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $contentitems = []; $contentitems[] = [ 'type' => 'ltiResourceLink', 'url' => 'http://example.com/messages/launch', 'title' => 'Test title', 'text' => 'Test text', 'icon' => [ 'url' => 'http://lti.example.com/image.jpg', 'width' => 100 ], 'frame' => [] ]; $contentitemsjson13 = json_encode($contentitems); $json11 = lti_convert_content_items($contentitemsjson13); $config = lti_tool_configuration_from_content_item($typeid, 'ContentItemSelection', $type->ltiversion, 'ConsumerKey', $json11); $this->assertEquals($contentitems[0]['title'], $config->name); $this->assertEquals($contentitems[0]['text'], $config->introeditor['text']); $this->assertEquals($contentitems[0]['url'], $config->toolurl); $this->assertEquals($contentitems[0]['icon']['url'], $config->icon); $this->assertEquals(LTI_SETTING_NEVER, $config->instructorchoiceacceptgrades); } /** * Test lti_sign_jwt(). */ public function test_lti_sign_jwt(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = 'consumerkey'; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $typeid = lti_add_type($type, $config); $params = []; $params['roles'] = 'urn:lti:role:ims/lis/testrole,' . 'urn:lti:instrole:ims/lis/testinstrole,' . 'urn:lti:sysrole:ims/lis/testsysrole,' . 'hi'; $params['accept_copy_advice'] = [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_copy_advice', 'isarray' => false ]; $params['lis_result_sourcedid'] = [ 'suffix' => 'bos', 'group' => 'basicoutcomesservice', 'claim' => 'lis_result_sourcedid', 'isarray' => false ]; $endpoint = 'https://www.example.com/moodle'; $oauthconsumerkey = 'consumerkey'; $nonce = ''; $jwt = lti_sign_jwt($params, $endpoint, $oauthconsumerkey, $typeid, $nonce); $this->assertArrayHasKey('id_token', $jwt); $this->assertNotEmpty($jwt['id_token']); } /** * Test lti_convert_from_jwt() */ public function test_lti_convert_from_jwt(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = 'sso.example.com'; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $config->lti_publickey = '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----'; $config->lti_keytype = LTI_RSA_KEY; $typeid = lti_add_type($type, $config); $params = lti_convert_from_jwt($typeid, 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwib' . 'mFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic3NvLmV4YW1wbGUuY29tIn0.XURVvEb5ueAvFsn-S9EB' . 'BSfKbsgUzfRQqmJ6evlrYdx7sXWoZXw1nYjaLTg-mawvBr7MVvrdG9qh6oN8OfkQ7bfMwiz4tjBMJ4B4q_sig5BDYIKwMNjZL5GGCBs89FQrgqZBhxw' . '3exTjPBEn69__w40o0AhCsBohPMh0ZsAyHug5dhm8vIuOP667repUJzM8uKCD6L4bEL6vQE8EwU6WQOmfJ2SDmRs-1pFkiaFd6hmPn6AVX7ETtzQmlT' . 'X-nXe9weQjU1lH4AQG2Yfnn-7lS94bt6E76Zt-XndP3IY7W48EpnRfUK9Ff1fZlomT4MPahdNP1eP8gT2iMz7vYpCfmA'); $this->assertEquals('sso.example.com', $params['oauth_consumer_key']); $this->assertEquals('John Doe', $params['lis_person_name_full']); } /** * Test lti_get_permitted_service_scopes(). */ public function test_lti_get_permitted_service_scopes(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $typeconfig = new \stdClass(); $typeconfig->lti_acceptgrades = true; $typeid = lti_add_type($type, $typeconfig); $tool = lti_get_type($typeid); $config = lti_get_type_config($typeid); $permittedscopes = lti_get_permitted_service_scopes($tool, $config); $expected = [ 'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome' ]; $this->assertEquals($expected, $permittedscopes); } /** * Test get_tool_type_config(). */ public function test_get_tool_type_config(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $typeid = lti_add_type($type, $config); $type = lti_get_type($typeid); $typeconfig = get_tool_type_config($type); $this->assertEquals('https://www.example.com/moodle', $typeconfig['platformid']); $this->assertEquals($type->clientid, $typeconfig['clientid']); $this->assertEquals($typeid, $typeconfig['deploymentid']); $this->assertEquals('https://www.example.com/moodle/mod/lti/certs.php', $typeconfig['publickeyseturl']); $this->assertEquals('https://www.example.com/moodle/mod/lti/token.php', $typeconfig['accesstokenurl']); $this->assertEquals('https://www.example.com/moodle/mod/lti/auth.php', $typeconfig['authrequesturl']); } /** * Test lti_new_access_token(). */ public function test_lti_new_access_token(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Create a tool type, associated with that proxy. $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $config = new \stdClass(); $typeid = lti_add_type($type, $config); $scopes = ['lti_some_scope', 'lti_another_scope']; lti_new_access_token($typeid, $scopes); $token = $DB->get_records('lti_access_tokens'); $this->assertEquals(1, count($token)); $token = reset($token); $this->assertEquals($typeid, $token->typeid); $this->assertEquals(json_encode(array_values($scopes)), $token->scope); $this->assertEquals($token->timecreated + LTI_ACCESS_TOKEN_LIFE, $token->validuntil); $this->assertNull($token->lastaccess); } /** * Test lti_build_login_request(). */ public function test_lti_build_login_request(): void { global $USER, $CFG; $this->resetAfterTest(); $USER->id = 123456789; $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module('lti', [ 'course' => $course->id, ] ); $config = new \stdClass(); $config->lti_clientid = 'some-client-id'; $config->typeid = 'some-type-id'; $config->lti_toolurl = 'some-lti-tool-url'; $request = lti_build_login_request($course->id, $instance->cmid, $instance, $config, 'basic-lti-launch-request'); $this->assertEquals($CFG->wwwroot, $request['iss']); $this->assertEquals('http://some-lti-tool-url', $request['target_link_uri']); $this->assertEquals(123456789, $request['login_hint']); $this->assertTrue(strpos($request['lti_message_hint'], "\"cmid\":{$instance->cmid}") > 0); $this->assertTrue(strpos($request['lti_message_hint'], "\"launchid\":\"ltilaunch{$instance->id}_") > 0); $this->assertEquals('some-client-id', $request['client_id']); $this->assertEquals('some-type-id', $request['lti_deployment_id']); } /** * @covers ::lti_get_launch_data() * * Test for_user is passed as parameter when specified. */ public function test_lti_get_launch_data_with_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $config = new \stdClass(); $config->lti_organizationid = ''; $course = $this->getDataGenerator()->create_course(); $type = $this->create_type($config); $link = $this->create_instance($type, $course); $launchdata = lti_get_launch_data($link, '', '', 345); $this->assertEquals($launchdata[1]['lti_message_type'], 'basic-lti-launch-request'); $this->assertEquals($launchdata[1]['for_user_id'], 345); } /** * Test default orgid is host if not specified in config (tool installed in earlier version of Moodle). */ public function test_lti_get_launch_data_default_organizationid_unset_usehost(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $config = new \stdClass(); $config->lti_organizationid = ''; $course = $this->getDataGenerator()->create_course(); $type = $this->create_type($config); $link = $this->create_instance($type, $course); $launchdata = lti_get_launch_data($link); $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com'); } /** * Test default org id is set to host when config is usehost. */ public function test_lti_get_launch_data_default_organizationid_set_usehost(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $config = new \stdClass(); $config->lti_organizationid = ''; $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST; $course = $this->getDataGenerator()->create_course(); $type = $this->create_type($config); $link = $this->create_instance($type, $course); $launchdata = lti_get_launch_data($link); $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'www.example.com'); } /** * Test default org id is set to site id when config is usesiteid. */ public function test_lti_get_launch_data_default_organizationid_set_usesiteid(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $config = new \stdClass(); $config->lti_organizationid = ''; $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID; $course = $this->getDataGenerator()->create_course(); $type = $this->create_type($config); $link = $this->create_instance($type, $course); $launchdata = lti_get_launch_data($link); $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], md5(get_site_identifier())); } /** * Test orgid can be overridden in which case default is ignored. */ public function test_lti_get_launch_data_default_organizationid_orgid_override(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $config = new \stdClass(); $config->lti_organizationid = 'overridden!'; $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID; $course = $this->getDataGenerator()->create_course(); $type = $this->create_type($config); $link = $this->create_instance($type, $course); $launchdata = lti_get_launch_data($link); $this->assertEquals($launchdata[1]['tool_consumer_instance_guid'], 'overridden!'); } public function test_get_course_history(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $parentparentcourse = $this->getDataGenerator()->create_course(); $parentcourse = $this->getDataGenerator()->create_course(); $parentcourse->originalcourseid = $parentparentcourse->id; $DB->update_record('course', $parentcourse); $course = $this->getDataGenerator()->create_course(); $course->originalcourseid = $parentcourse->id; $DB->update_record('course', $course); $this->assertEquals(get_course_history($parentparentcourse), []); $this->assertEquals(get_course_history($parentcourse), [$parentparentcourse->id]); $this->assertEquals(get_course_history($course), [$parentcourse->id, $parentparentcourse->id]); $course->originalcourseid = 38903; $DB->update_record('course', $course); $this->assertEquals(get_course_history($course), [38903]); } /** * Test the lti_get_ims_role helper function. * * @dataProvider lti_get_ims_role_provider * @covers ::lti_get_ims_role() * * @param bool $islti2 whether the method is called with LTI 2.0 role names or not. * @param string $rolename the name of the role (student, teacher, admin) * @param null|string $switchedto the role to switch to, or false if not using the 'switch to' functionality. * @param string $expected the expected role name. */ public function test_lti_get_ims_role(bool $islti2, string $rolename, ?string $switchedto, string $expected): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $rolename == 'admin' ? get_admin() : $this->getDataGenerator()->create_and_enrol($course, $rolename); if ($switchedto) { $this->setUser($user); $role = $DB->get_record('role', array('shortname' => $switchedto)); role_switch($role->id, \context_course::instance($course->id)); } $this->assertEquals($expected, lti_get_ims_role($user, 0, $course->id, $islti2)); } /** * Data provider for testing lti_get_ims_role. * * @return array[] the test case data. */ public static function lti_get_ims_role_provider(): array { return [ 'Student, LTI 1.1, no role switch' => [ 'islti2' => false, 'rolename' => 'student', 'switchedto' => null, 'expected' => 'Learner' ], 'Student, LTI 2.0, no role switch' => [ 'islti2' => true, 'rolename' => 'student', 'switchedto' => null, 'expected' => 'Learner' ], 'Teacher, LTI 1.1, no role switch' => [ 'islti2' => false, 'rolename' => 'editingteacher', 'switchedto' => null, 'expected' => 'Instructor' ], 'Teacher, LTI 2.0, no role switch' => [ 'islti2' => true, 'rolename' => 'editingteacher', 'switchedto' => null, 'expected' => 'Instructor' ], 'Admin, LTI 1.1, no role switch' => [ 'islti2' => false, 'rolename' => 'admin', 'switchedto' => null, 'expected' => 'Instructor,urn:lti:sysrole:ims/lis/Administrator,urn:lti:instrole:ims/lis/Administrator' ], 'Admin, LTI 2.0, no role switch' => [ 'islti2' => true, 'rolename' => 'admin', 'switchedto' => null, 'expected' => 'Instructor,http://purl.imsglobal.org/vocab/lis/v2/person#Administrator' ], 'Admin, LTI 1.1, role switch student' => [ 'islti2' => false, 'rolename' => 'admin', 'switchedto' => 'student', 'expected' => 'Learner' ], 'Admin, LTI 2.0, role switch student' => [ 'islti2' => true, 'rolename' => 'admin', 'switchedto' => 'student', 'expected' => 'Learner' ], 'Admin, LTI 1.1, role switch teacher' => [ 'islti2' => false, 'rolename' => 'admin', 'switchedto' => 'editingteacher', 'expected' => 'Instructor' ], 'Admin, LTI 2.0, role switch teacher' => [ 'islti2' => true, 'rolename' => 'admin', 'switchedto' => 'editingteacher', 'expected' => 'Instructor' ], ]; } /** * Test lti_get_lti_types_and_proxies with no limit or offset. */ public function test_lti_get_lti_types_and_proxies_with_no_limit(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->generate_tool_types_and_proxies(10); list($proxies, $types) = lti_get_lti_types_and_proxies(); $this->assertCount(10, $proxies); $this->assertCount(10, $types); } /** * Test lti_get_lti_types_and_proxies with limits. */ public function test_lti_get_lti_types_and_proxies_with_limit(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->generate_tool_types_and_proxies(10); // Get the middle 10 data sets (of 20 total). list($proxies, $types) = lti_get_lti_types_and_proxies(10, 5); $this->assertCount(5, $proxies); $this->assertCount(5, $types); // Get the last 5 data sets with large limit (of 20 total). list($proxies, $types) = lti_get_lti_types_and_proxies(50, 15); $this->assertCount(0, $proxies); $this->assertCount(5, $types); // Get the last 13 data sets with large limit (of 20 total). list($proxies, $types) = lti_get_lti_types_and_proxies(50, 7); $this->assertCount(3, $proxies); $this->assertCount(10, $types); } /** * Test lti_get_lti_types_and_proxies with limits and only fetching orphaned proxies. */ public function test_lti_get_lti_types_and_proxies_with_limit_and_orphaned_proxies(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->generate_tool_types_and_proxies(10, 5); // Get the first 10 data sets (of 15 total). list($proxies, $types) = lti_get_lti_types_and_proxies(10, 0, true); $this->assertCount(5, $proxies); $this->assertCount(5, $types); // Get the middle 10 data sets with large limit (of 15 total). list($proxies, $types) = lti_get_lti_types_and_proxies(10, 2, true); $this->assertCount(3, $proxies); $this->assertCount(7, $types); // Get the last 5 data sets with large limit (of 15 total). list($proxies, $types) = lti_get_lti_types_and_proxies(50, 10, true); $this->assertCount(0, $proxies); $this->assertCount(5, $types); } /** * Test lti_get_lti_types_and_proxies_count. */ public function test_lti_get_lti_types_and_proxies_count_with_no_filters(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->generate_tool_types_and_proxies(10, 5); $totalcount = lti_get_lti_types_and_proxies_count(); $this->assertEquals(25, $totalcount); // 10 types, 15 proxies. } /** * Test lti_get_lti_types_and_proxies_count only counting orphaned proxies. */ public function test_lti_get_lti_types_and_proxies_count_with_only_orphaned_proxies(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->generate_tool_types_and_proxies(10, 5); $orphanedcount = lti_get_lti_types_and_proxies_count(true); $this->assertEquals(15, $orphanedcount); // 10 types, 5 proxies. } /** * Test lti_get_lti_types_and_proxies_count only matching tool type with toolproxyid. */ public function test_lti_get_lti_types_and_proxies_count_type_with_proxyid(): void { $this->resetAfterTest(); $this->setAdminUser(); ['proxies' => $proxies, 'types' => $types] = $this->generate_tool_types_and_proxies(10, 5); $countwithproxyid = lti_get_lti_types_and_proxies_count(false, $proxies[0]->id); $this->assertEquals(16, $countwithproxyid); // 1 type, 15 proxies. } /** * Verify that empty curl responses lead to the proper moodle_exception, not to XML ValueError. * * @covers ::lti_load_cartridge() */ public function test_empty_reponse_lti_load_cartridge(): void { // Mock the curl response to empty string, this is hardly // reproducible in real life (only Windows + GHA). \curl::mock_response(''); $this->expectException(\moodle_exception::class); lti_load_cartridge('http://example.com/mocked/empty/response', []); } /** * Create an LTI Tool. * * @param object $config tool config. * * @return object tool. */ private function create_type(object $config) { $type = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->clientid = "Test client ID"; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); $configbase = new \stdClass(); $configbase->lti_acceptgrades = LTI_SETTING_NEVER; $configbase->lti_sendname = LTI_SETTING_NEVER; $configbase->lti_sendemailaddr = LTI_SETTING_NEVER; $mergedconfig = (object) array_merge( (array) $configbase, (array) $config); $typeid = lti_add_type($type, $mergedconfig); return lti_get_type($typeid); } /** * Create an LTI Instance for the tool in a given course. * * @param object $type tool for which an instance should be added. * @param object $course course where the instance should be added. * * @return object instance. */ private function create_instance(object $type, object $course) { $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); return $generator->create_instance(array('course' => $course->id, 'toolurl' => $type->baseurl, 'typeid' => $type->id ), array()); } /** * Generate a number of LTI tool types and proxies. * * @param int $toolandproxycount How many tool types and associated proxies to create. E.g. Value of 10 will create 10 types * and 10 proxies. * @param int $orphanproxycount How many orphaned proxies to create. * @return array[] */ private function generate_tool_types_and_proxies(int $toolandproxycount = 0, int $orphanproxycount = 0) { $proxies = []; $types = []; for ($i = 0; $i < $toolandproxycount; $i++) { $proxies[$i] = $this->generate_tool_proxy($i); $types[$i] = $this->generate_tool_type($i, $proxies[$i]->id); } for ($i = $toolandproxycount; $i < ($toolandproxycount + $orphanproxycount); $i++) { $proxies[$i] = $this->generate_tool_proxy($i); } return ['proxies' => $proxies, 'types' => $types]; } /** * Test for lti_get_lti_types_by_course. * * Note: This includes verification of the broken legacy behaviour in which the inclusion of course and site tools could be * controlled independently, based on the capabilities 'mod/lti:addmanualinstance' (to include course tools) and * 'mod/lti:addpreconfiguredinstance' (to include site tools). This behaviour is deprecated in 4.3 and all preconfigured tools * are controlled by the single capability 'mod/lti:addpreconfiguredinstance'. * * @covers ::lti_get_lti_types_by_course() * @return void */ public function test_lti_get_lti_types_by_course(): void { $this->resetAfterTest(); global $DB; $coursecat1 = $this->getDataGenerator()->create_category(); $coursecat2 = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]); $course2 = $this->getDataGenerator()->create_course(['category' => $coursecat2->id]); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course2, 'editingteacher'); // Create the following tool types for testing: // - Site tool configured as "Do not show" (LTI_COURSEVISIBLE_NO). // - Site tool configured as "Show as a preconfigured tool only" (LTI_COURSEVISIBLE_PRECONFIGURED). // - Site tool configured as "Show as a preconfigured tool and in the activity chooser" (LTI_COURSEVISIBLE_ACTIVITYCHOOSER). // - Course tool which, by default, is configured as LTI_COURSEVISIBLE_ACTIVITYCHOOSER). // - Site tool configured to "Show as a preconfigured tool and in the activity chooser" but restricted to a category. /** @var \mod_lti_generator $ltigenerator */ $ltigenerator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $ltigenerator->create_tool_types([ 'name' => 'site tool do not show', 'baseurl' => 'http://example.com/tool/1', 'coursevisible' => LTI_COURSEVISIBLE_NO, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured only', 'baseurl' => 'http://example.com/tool/2', 'coursevisible' => LTI_COURSEVISIBLE_PRECONFIGURED, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser', 'baseurl' => 'http://example.com/tool/3', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED ]); $ltigenerator->create_course_tool_types([ 'name' => 'course tool preconfigured and activity chooser', 'baseurl' => 'http://example.com/tool/4', 'course' => $course->id ]); $ltigenerator->create_tool_types([ 'name' => 'site tool preconfigured and activity chooser, restricted to category 2', 'baseurl' => 'http://example.com/tool/5', 'coursevisible' => LTI_COURSEVISIBLE_ACTIVITYCHOOSER, 'state' => LTI_TOOL_STATE_CONFIGURED, 'lti_coursecategories' => $coursecat2->id ]); $this->setUser($teacher); // Important: this deprecated method depends on the global user for cap checks. // Request using the default 'coursevisible' param will include all tools except the one configured as "Do not show". $coursetooltypes = lti_get_lti_types_by_course($course->id); $this->assertDebuggingCalled(); $this->assertCount(3, $coursetooltypes); $this->assertEmpty(array_diff( ['http://example.com/tool/2', 'http://example.com/tool/3', 'http://example.com/tool/4'], array_column($coursetooltypes, 'baseurl') )); // Request for only those tools configured to show in the activity chooser for the teacher. $coursetooltypes = lti_get_lti_types_by_course($course->id, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]); $this->assertDebuggingCalled(); $this->assertCount(2, $coursetooltypes); $this->assertEmpty(array_diff( ['http://example.com/tool/3', 'http://example.com/tool/4'], array_column($coursetooltypes, 'baseurl') )); // Request for only those tools configured to show as a preconfigured tool for the teacher. $coursetooltypes = lti_get_lti_types_by_course($course->id, [LTI_COURSEVISIBLE_PRECONFIGURED]); $this->assertDebuggingCalled(); $this->assertCount(1, $coursetooltypes); $this->assertEmpty(array_diff( ['http://example.com/tool/2'], array_column($coursetooltypes, 'baseurl') )); // Request for teacher2 in course2 (course category 2). $this->setUser($teacher2); $coursetooltypes = lti_get_lti_types_by_course($course2->id); $this->assertDebuggingCalled(); $this->assertCount(3, $coursetooltypes); $this->assertEmpty(array_diff( ['http://example.com/tool/2', 'http://example.com/tool/3', 'http://example.com/tool/5'], array_column($coursetooltypes, 'baseurl') )); // Request for a teacher who cannot use preconfigured tools in the course. // No tools are available. $this->setUser($teacher); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); assign_capability('mod/lti:addpreconfiguredinstance', CAP_PROHIBIT, $teacherrole->id, \core\context\course::instance($course->id)); $coursetooltypes = lti_get_lti_types_by_course($course->id); $this->assertDebuggingCalled(); $this->assertCount(0, $coursetooltypes); $this->unassignUserCapability('mod/lti:addpreconfiguredinstance', (\core\context\course::instance($course->id))->id, $teacherrole->id); } } tests/externallib_test.php000064400000060065152265520610012002 0ustar00. namespace mod_lti; use core_external\external_api; use mod_lti_external; use mod_lti_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/mod/lti/lib.php'); require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); /** * External tool module external functions tests * * @package mod_lti * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ final class externallib_test extends mod_lti_testcase { /** * Set up for every test */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Sets up some basic test data including course, users, roles, and an lti instance, for use in some tests. * @return array */ protected function setup_test_data() { global $DB; $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $lti = $this->getDataGenerator()->create_module( 'lti', ['course' => $course->id, 'toolurl' => 'http://localhost/not/real/tool.php'] ); $context = \context_module::instance($lti->cmid); $cm = get_coursemodule_from_instance('lti', $lti->id); // Create users. $student = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); // Users enrolments. $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual'); return [ 'course' => $course, 'lti' => $lti, 'context' => $context, 'cm' => $cm, 'student' => $student, 'teacher' => $teacher, 'studentrole' => $studentrole, 'teacherrole' => $teacherrole ]; } /** * Test get_tool_proxies. */ public function test_mod_lti_get_tool_proxies(): void { // Create two tool proxies. One to associate with tool, and one to leave orphaned. $this->setAdminUser(); $proxy = $this->generate_tool_proxy("1"); $orphanedproxy = $this->generate_tool_proxy("2"); $this->generate_tool_type("1", $proxy->id); // Associate proxy 1 with tool type. // Fetch all proxies. $proxies = mod_lti_external::get_tool_proxies(false); $proxies = external_api::clean_returnvalue(mod_lti_external::get_tool_proxies_returns(), $proxies); $this->assertCount(2, $proxies); $this->assertEqualsCanonicalizing([(array) $proxy, (array) $orphanedproxy], $proxies); } /** * Test get_tool_proxies with orphaned proxies only. */ public function test_mod_lti_get_orphaned_tool_proxies(): void { // Create two tool proxies. One to associate with tool, and one to leave orphaned. $this->setAdminUser(); $proxy = $this->generate_tool_proxy("1"); $orphanedproxy = $this->generate_tool_proxy("2"); $this->generate_tool_type("1", $proxy->id); // Associate proxy 1 with tool type. // Fetch all proxies. $proxies = mod_lti_external::get_tool_proxies(true); $proxies = external_api::clean_returnvalue(mod_lti_external::get_tool_proxies_returns(), $proxies); $this->assertCount(1, $proxies); $this->assertEqualsCanonicalizing([(array) $orphanedproxy], $proxies); } /** * Test get_tool_launch_data. */ public function test_get_tool_launch_data(): void { global $USER; [ 'course' => $course, 'lti' => $lti ] = $this->setup_test_data(); $result = mod_lti_external::get_tool_launch_data($lti->id); $result = external_api::clean_returnvalue(mod_lti_external::get_tool_launch_data_returns(), $result); // Basic test, the function returns what it's expected. self::assertEquals($lti->toolurl, $result['endpoint']); self::assertCount(36, $result['parameters']); // Check some parameters. $parameters = array(); foreach ($result['parameters'] as $param) { $parameters[$param['name']] = $param['value']; } self::assertEquals($lti->resourcekey, $parameters['oauth_consumer_key']); self::assertEquals($course->fullname, $parameters['context_title']); self::assertEquals($course->shortname, $parameters['context_label']); self::assertEquals($USER->id, $parameters['user_id']); self::assertEquals($USER->firstname, $parameters['lis_person_name_given']); self::assertEquals($USER->lastname, $parameters['lis_person_name_family']); self::assertEquals(fullname($USER), $parameters['lis_person_name_full']); self::assertEquals($USER->username, $parameters['ext_user_username']); self::assertEquals("phpunit", $parameters['tool_consumer_instance_name']); self::assertEquals("PHPUnit test site", $parameters['tool_consumer_instance_description']); } /** * Test get_ltis_by_courses. */ public function test_mod_lti_get_ltis_by_courses(): void { [ 'course' => $course, 'lti' => $lti, 'student' => $student, 'teacher' => $teacher, 'studentrole' => $studentrole, ] = $this->setup_test_data(); // Create additional course. $course2 = self::getDataGenerator()->create_course(); // Second lti. $record = new \stdClass(); $record->course = $course2->id; $lti2 = self::getDataGenerator()->create_module('lti', $record); // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. $enrol = enrol_get_plugin('manual'); $enrolinstances = enrol_get_instances($course2->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance2 = $courseenrolinstance; break; } } $enrol->enrol_user($instance2, $student->id, $studentrole->id); self::setUser($student); $returndescription = mod_lti_external::get_ltis_by_courses_returns(); // Create what we expect to be returned when querying the two courses. // First for the student user. $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'launchcontainer', 'showtitlelaunch', 'showdescriptionlaunch', 'icon', 'secureicon'); // Add expected coursemodule and data. $lti1 = $lti; $lti1->coursemodule = $lti1->cmid; $lti1->introformat = 1; $lti1->section = 0; $lti1->visible = true; $lti1->groupmode = 0; $lti1->groupingid = 0; $lti1->section = 0; $lti1->introfiles = []; $lti1->lang = ''; $lti2->coursemodule = $lti2->cmid; $lti2->introformat = 1; $lti2->section = 0; $lti2->visible = true; $lti2->groupmode = 0; $lti2->groupingid = 0; $lti2->section = 0; $lti2->introfiles = []; $lti2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $lti1->{$field}; $expected2[$field] = $lti2->{$field}; } $expectedltis = array($expected2, $expected1); // Call the external function passing course ids. $result = mod_lti_external::get_ltis_by_courses(array($course2->id, $course->id)); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedltis, $result['ltis']); $this->assertCount(0, $result['warnings']); // Call the external function without passing course id. $result = mod_lti_external::get_ltis_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedltis, $result['ltis']); $this->assertCount(0, $result['warnings']); // Unenrol user from second course and alter expected ltis. $enrol->unenrol_user($instance2, $student->id); array_shift($expectedltis); // Call the external function without passing course id. $result = mod_lti_external::get_ltis_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedltis, $result['ltis']); // Call for the second course we unenrolled the user from, expected warning. $result = mod_lti_external::get_ltis_by_courses(array($course2->id)); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertCount(1, $result['warnings']); $this->assertEquals('1', $result['warnings'][0]['warningcode']); $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); // Now, try as a teacher for getting all the additional fields. self::setUser($teacher); $additionalfields = array('timecreated', 'timemodified', 'typeid', 'toolurl', 'securetoolurl', 'instructorchoicesendname', 'instructorchoicesendemailaddr', 'instructorchoiceallowroster', 'instructorchoiceallowsetting', 'instructorcustomparameters', 'instructorchoiceacceptgrades', 'grade', 'resourcekey', 'password', 'debuglaunch', 'servicesalt', 'visible', 'groupmode', 'groupingid', 'section', 'lang'); foreach ($additionalfields as $field) { $expectedltis[0][$field] = $lti1->{$field}; } $result = mod_lti_external::get_ltis_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedltis, $result['ltis']); // Admin also should get all the information. self::setAdminUser(); $result = mod_lti_external::get_ltis_by_courses(array($course->id)); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedltis, $result['ltis']); // Now, prohibit capabilities. $this->setUser($student); $contextcourse1 = \context_course::instance($course->id); // Prohibit capability = mod:lti:view on Course1 for students. assign_capability('mod/lti:view', CAP_PROHIBIT, $studentrole->id, $contextcourse1->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); $ltis = mod_lti_external::get_ltis_by_courses(array($course->id)); $ltis = external_api::clean_returnvalue(mod_lti_external::get_ltis_by_courses_returns(), $ltis); $this->assertCount(0, $ltis['ltis']); } /** * Test view_lti with an invalid instance id. */ public function test_view_lti_invalid_instanceid(): void { $this->expectException(\moodle_exception::class); mod_lti_external::view_lti(0); } /** * Test view_lti as a user who is not enrolled in the course. */ public function test_view_lti_no_enrolment(): void { [ 'lti' => $lti ] = $this->setup_test_data(); // Test not-enrolled user. $usernotenrolled = self::getDataGenerator()->create_user(); $this->setUser($usernotenrolled); $this->expectException(\moodle_exception::class); mod_lti_external::view_lti($lti->id); } /** * Test view_lti for a user without the mod/lti:view capability. */ public function test_view_lti_no_capability(): void { [ 'lti' => $lti, 'student' => $student, 'studentrole' => $studentrole, 'context' => $context, ] = $this->setup_test_data(); $this->setUser($student); // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. assign_capability('mod/lti:view', CAP_PROHIBIT, $studentrole->id, $context->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); $this->expectException(\moodle_exception::class); mod_lti_external::view_lti($lti->id); } /** * Test view_lti for a user with the mod/lti:view capability in the course. */ public function test_view_lti(): void { [ 'lti' => $lti, 'context' => $context, 'cm' => $cm, 'student' => $student, ] = $this->setup_test_data(); // Test user with full capabilities. $this->setUser($student); // Trigger and capture the event. $sink = $this->redirectEvents(); $result = mod_lti_external::view_lti($lti->id); // The value of the result isn't needed but validation is. external_api::clean_returnvalue(mod_lti_external::view_lti_returns(), $result); $events = $sink->get_events(); $this->assertCount(1, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_lti\event\course_module_viewed', $event); $this->assertEquals($context, $event->get_context()); $moodlelti = new \moodle_url('/mod/lti/view.php', array('id' => $cm->id)); $this->assertEquals($moodlelti, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test create_tool_proxy. */ public function test_mod_lti_create_tool_proxy(): void { $this->setAdminUser(); $capabilities = ['AA', 'BB']; $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), $capabilities, []); $proxy = (object) external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy); $this->assertEquals('Test proxy', $proxy->name); $this->assertEquals($this->getExternalTestFileUrl('/test.html'), $proxy->regurl); $this->assertEquals(LTI_TOOL_PROXY_STATE_PENDING, $proxy->state); $this->assertEquals(implode("\n", $capabilities), $proxy->capabilityoffered); } /** * Test create_tool_proxy with a duplicate url. */ public function test_mod_lti_create_tool_proxy_duplicateurl(): void { $this->setAdminUser(); mod_lti_external::create_tool_proxy('Test proxy 1', $this->getExternalTestFileUrl('/test.html'), array(), array()); $this->expectException(\moodle_exception::class); mod_lti_external::create_tool_proxy('Test proxy 2', $this->getExternalTestFileUrl('/test.html'), array(), array()); } /** * Test create_tool_proxy for a user without the required capability. */ public function test_mod_lti_create_tool_proxy_without_capability(): void { $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($teacher); $this->expectException(\required_capability_exception::class); mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); } /** * Test delete_tool_proxy. */ public function test_mod_lti_delete_tool_proxy(): void { $this->setAdminUser(); $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); $proxy = (object) external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy); $this->assertNotEmpty(lti_get_tool_proxy($proxy->id)); $proxy = mod_lti_external::delete_tool_proxy($proxy->id); $proxy = (object) external_api::clean_returnvalue(mod_lti_external::delete_tool_proxy_returns(), $proxy); $this->assertEquals('Test proxy', $proxy->name); $this->assertEquals($this->getExternalTestFileUrl('/test.html'), $proxy->regurl); $this->assertEquals(LTI_TOOL_PROXY_STATE_PENDING, $proxy->state); $this->assertEmpty(lti_get_tool_proxy($proxy->id)); } /** * Test get_tool_proxy_registration_request. */ public function test_mod_lti_get_tool_proxy_registration_request(): void { $this->setAdminUser(); $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); $proxy = (object) external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy); $request = mod_lti_external::get_tool_proxy_registration_request($proxy->id); $request = external_api::clean_returnvalue(mod_lti_external::get_tool_proxy_registration_request_returns(), $request); $this->assertEquals('ToolProxyRegistrationRequest', $request['lti_message_type']); $this->assertEquals('LTI-2p0', $request['lti_version']); } /** * Test get_tool_types. */ public function test_mod_lti_get_tool_types(): void { $this->setAdminUser(); $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); $proxy = (object) external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy); // Create a tool type, associated with that proxy. $type = new \stdClass(); $data = new \stdClass(); $type->state = LTI_TOOL_STATE_CONFIGURED; $type->name = "Test tool"; $type->description = "Example description"; $type->toolproxyid = $proxy->id; $type->baseurl = $this->getExternalTestFileUrl('/test.html'); lti_add_type($type, $data); $types = mod_lti_external::get_tool_types($proxy->id); $types = external_api::clean_returnvalue(mod_lti_external::get_tool_types_returns(), $types); $this->assertCount(1, $types); $type = $types[0]; $this->assertEquals('Test tool', $type['name']); $this->assertEquals('Example description', $type['description']); } /** * Test create_tool_type. */ public function test_mod_lti_create_tool_type(): void { $this->setAdminUser(); $type = mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'), '', ''); $type = external_api::clean_returnvalue(mod_lti_external::create_tool_type_returns(), $type); $this->assertEquals('Example tool', $type['name']); $this->assertEquals('Example tool description', $type['description']); $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $type['urls']['icon']); $typeentry = lti_get_type($type['id']); $this->assertEquals('http://www.example.com/lti/provider.php', $typeentry->baseurl); $config = lti_get_type_config($type['id']); $this->assertTrue(isset($config['sendname'])); $this->assertTrue(isset($config['sendemailaddr'])); $this->assertTrue(isset($config['acceptgrades'])); $this->assertTrue(isset($config['forcessl'])); } /** * Test create_tool_type failure from non existent file. */ public function test_mod_lti_create_tool_type_nonexistant_file(): void { $this->expectException(\moodle_exception::class); mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/doesntexist.xml'), '', ''); } /** * Test create_tool_type failure from xml that is not a cartridge. */ public function test_mod_lti_create_tool_type_bad_file(): void { $this->expectException(\moodle_exception::class); mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/rsstest.xml'), '', ''); } /** * Test create_tool_type as a user without the required capability. */ public function test_mod_lti_create_tool_type_without_capability(): void { $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($teacher); $this->expectException(\required_capability_exception::class); mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'), '', ''); } /** * Test update_tool_type. */ public function test_mod_lti_update_tool_type(): void { $this->setAdminUser(); $type = mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'), '', ''); $type = external_api::clean_returnvalue(mod_lti_external::create_tool_type_returns(), $type); $type = mod_lti_external::update_tool_type($type['id'], 'New name', 'New description', LTI_TOOL_STATE_PENDING); $type = external_api::clean_returnvalue(mod_lti_external::update_tool_type_returns(), $type); $this->assertEquals('New name', $type['name']); $this->assertEquals('New description', $type['description']); $this->assertEquals('Pending', $type['state']['text']); } /** * Test delete_tool_type for a user with the required capability. */ public function test_mod_lti_delete_tool_type(): void { $this->setAdminUser(); $type = mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'), '', ''); $type = external_api::clean_returnvalue(mod_lti_external::create_tool_type_returns(), $type); $this->assertNotEmpty(lti_get_type($type['id'])); $type = mod_lti_external::delete_tool_type($type['id']); $type = external_api::clean_returnvalue(mod_lti_external::delete_tool_type_returns(), $type); $this->assertEmpty(lti_get_type($type['id'])); } /** * Test delete_tool_type for a user without the required capability. */ public function test_mod_lti_delete_tool_type_without_capability(): void { $this->setAdminUser(); $type = mod_lti_external::create_tool_type($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml'), '', ''); $type = external_api::clean_returnvalue(mod_lti_external::create_tool_type_returns(), $type); $this->assertNotEmpty(lti_get_type($type['id'])); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->setUser($teacher); $this->expectException(\required_capability_exception::class); mod_lti_external::delete_tool_type($type['id']); } /** * Test is_cartridge. */ public function test_mod_lti_is_cartridge(): void { $this->setAdminUser(); $result = mod_lti_external::is_cartridge($this->getExternalTestFileUrl('/ims_cartridge_basic_lti_link.xml')); $result = external_api::clean_returnvalue(mod_lti_external::is_cartridge_returns(), $result); $this->assertTrue($result['iscartridge']); $result = mod_lti_external::is_cartridge($this->getExternalTestFileUrl('/test.html')); $result = external_api::clean_returnvalue(mod_lti_external::is_cartridge_returns(), $result); $this->assertFalse($result['iscartridge']); } } tests/event/unknown_service_api_called_test.php000064400000004357152265520610016170 0ustar00. namespace mod_lti\event; /** * Unknown service API called event tests * * @package mod_lti * @copyright Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class unknown_service_api_called_test extends \advanced_testcase { /* * Ensure create event works. */ public function test_create_event(): void { $event = unknown_service_api_called::create(); $this->assertInstanceOf('\mod_lti\event\unknown_service_api_called', $event); } /* * Ensure event context works. */ public function test_event_context(): void { $event = unknown_service_api_called::create(); $this->assertEquals(\context_system::instance(), $event->get_context()); } /* * Ensure we can trigger the event. */ public function test_trigger_event(): void { $event = unknown_service_api_called::create(); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); } /* * Ensure get/set message data is functioning as expected. */ public function test_get_message_data(): void { $data = (object) array( 'foo' => 'bar', 'bat' => 'baz', ); /* * @var unknown_service_api_called $event */ $event = unknown_service_api_called::create(); $event->set_message_data($data); $this->assertSame($data, $event->get_message_data()); } } tests/task/clean_access_tokens_test.php000064400000004212152265520610014411 0ustar00. namespace mod_lti\task; /** * Tests cleaning up the access tokens task. * * @package mod_lti * @category test * @copyright 2019 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class clean_access_tokens_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the cleanup task. */ public function test_cleanup_task(): void { global $DB; $time = time(); // Create an expired access token. $token = new \stdClass(); $token->typeid = 1; $token->scope = 'scope'; $token->token = 'token'; $token->validuntil = $time - DAYSECS; $token->timecreated = $time - DAYSECS; $t1id = $DB->insert_record('lti_access_tokens', $token); // New token, in the future. $token->validuntil = $time + DAYSECS; $token->token = 'token2'; $t2id = $DB->insert_record('lti_access_tokens', $token); // Run the task. $task = new clean_access_tokens(); $task->execute(); // Check there is only one token now. $tokens = $DB->get_records('lti_access_tokens'); $this->assertCount(1, $tokens); $token = reset($tokens); $this->assertEquals($t2id, $token->id); } } tests/service_exception_handler_test.php000064400000005420152265520610014676 0ustar00. namespace mod_lti; /** * Tests Exception handler for LTI services * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class service_exception_handler_test extends \advanced_testcase { /** * Testing service error handling. */ public function test_handle(): void { $handler = new service_exception_handler(false); $handler->set_message_id('123'); $handler->set_message_type('testRequest'); $handler->handle(new \Exception('Error happened')); $this->expectOutputRegex('/imsx_codeMajor>failure/'); $this->expectOutputRegex('/imsx_description>Error happened/'); $this->expectOutputRegex('/imsx_messageRefIdentifier>123/'); $this->expectOutputRegex('/imsx_operationRefIdentifier>testRequest/'); $this->expectOutputRegex('/imsx_POXBody>handle(new \Exception('Error happened')); $this->expectOutputRegex('/imsx_codeMajor>failure/'); $this->expectOutputRegex('/imsx_description>Error happened/'); $this->expectOutputRegex('/imsx_messageRefIdentifier\/>/'); $this->expectOutputRegex('/imsx_operationRefIdentifier>unknownRequest/'); $this->expectOutputRegex('/imsx_POXBody>resetAfterTest(); $handler = new service_exception_handler(true); ob_start(); $handler->handle(new \Exception('Error happened')); ob_end_clean(); $this->assertTrue(is_dir($CFG->dataroot.'/temp/mod_lti')); $files = glob($CFG->dataroot.'/temp/mod_lti/mod*'); $this->assertEquals(1, count($files)); } } tests/generator_test.php000064400000005273152265520610011457 0ustar00. namespace mod_lti; /** * PHPUnit data generator testcase * * @package mod_lti * @category phpunit * @copyright Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com) * @author Mark Nielsen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { public function test_generator(): void { global $DB; $this->resetAfterTest(true); $this->assertEquals(0, $DB->count_records('lti')); $course = $this->getDataGenerator()->create_course(); /* * @var mod_lti_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti'); $this->assertInstanceOf('mod_lti_generator', $generator); $this->assertEquals('lti', $generator->get_modulename()); $generator->create_instance(array('course' => $course->id)); $generator->create_instance(array('course' => $course->id)); $lti = $generator->create_instance(array('course' => $course->id)); $this->assertEquals(3, $DB->count_records('lti')); $cm = get_coursemodule_from_instance('lti', $lti->id); $this->assertEquals($lti->id, $cm->instance); $this->assertEquals('lti', $cm->modname); $this->assertEquals($course->id, $cm->course); $context = \context_module::instance($cm->id); $this->assertEquals($lti->cmid, $context->instanceid); // Test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE! $lti = $generator->create_instance(array('course' => $course->id, 'assessed' => 1, 'scale' => 100)); $gitem = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'lti', 'iteminstance' => $lti->id)); $this->assertNotEmpty($gitem); $this->assertEquals(100, $gitem->grademax); $this->assertEquals(0, $gitem->grademin); $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype); } } tests/behat/lti_activity_completion.feature000064400000005671152265520610015320 0ustar00@mod @mod_lti @core_completion Feature: View activity completion information in the LTI activity In order to have visibility of LTI completion requirements As a student I need to be able to view my LTI completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | showcompletionconditions | | Course 1 | C1 | 0 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | completion | completionview | completionusegrade | | lti | Music history | C1 | lti1 | 2 | 1 | 1 | Scenario: View automatic completion items as a teacher Given I am on the "Music history" "lti activity" page logged in as teacher1 Then "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "View" completion condition @javascript Scenario: View automatic completion items as a student Given I am on the "Music history" "lti activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "90.00" to the user "Vinnie Student1" for the grade item "Music history" And I press "Save changes" When I am on the "Music history" "lti activity" page logged in as student1 Then the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "View" completion condition of "Music history" is displayed as "done" @javascript Scenario: A student can manually mark the LTI activity as done but a teacher cannot Given I am on the "Music history" "lti activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And the manual completion button for "Music history" should be disabled # Student view. When I am on the "Music history" "lti activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" tests/behat/contentitem.feature000064400000003436152265520610012711 0ustar00@mod @mod_lti Feature: Content-Item support In order to easily add activities and content in a course from an external tool As a teacher I need to utilise a tool that supports the Deep Linking (Content-Item Message) type Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "mod_lti > tool types" exist: | name | description | baseurl | coursevisible | state | lti_contentitem | | Teaching Tool 1 | Tool 1 description | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | 1 | @javascript Scenario: Tool that supports Deep Linking should be able to configure a tool via the Select content button Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I add a "Teaching Tool 1" to section "1" using the activity chooser Then "Select content" "button" should be visible And the "Select content" "button" should be enabled Scenario: Editing the settings for an instance of a tool configured with Deep Linking support Given the following "mod_lti > tool instances" exist: | name | tool | course | | Test tool activity 1 | Teaching Tool 1 | C1 | When I am on the "Test tool activity 1" "lti activity editing" page logged in as teacher1 Then I should see "Select content" And the "Select content" "button" should be enabled tests/behat/renametool.feature000064400000001611152265520610012516 0ustar00@mod @mod_lti Feature: Rename external tools via inline editing In order to keep track of my activities As a teacher I need to be able to rename the LTI tool and have it's name change in the gradebook Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "activities" exist: | activity | course | name | | lti | C1 | Test tool activity 1 | @javascript Scenario: Add a tool and inline edit When I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I set the field "Edit title" in the "Test tool activity 1" "activity" to "Test tool activity renamed" And I navigate to "Setup > Gradebook setup" in the course gradebook Then I should not see "Test tool activity 1" And I should see "Test tool activity renamed" tests/behat/viewtools.feature000064400000002122152265520610012402 0ustar00@mod @mod_lti Feature: Navigate existing LTI tool types using pagination In order to manage reusable activities for teachers As an admin I need to view existing tools Background: Given 100 "mod_lti > tool types" exist with the following data: |name |Test tool [count] | |description |Example description [count] | |baseurl |https://www.example.com/tool[count]| And I log in as "admin" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration @javascript Scenario: View first page of tool types. Then I should see "Test tool 30" And "Test tool 70" "text" should not be visible @javascript Scenario: View second page of tool types using page 2 button. When I click on "2" "link" Then I should see "Test tool 70" And "Test tool 30" "text" should not be visible @javascript Scenario: View last page of tool types using page 2 button. When I click on "Last" "link" Then I should see "Test tool 70" And "Test tool 30" "text" should not be visible tests/behat/backup_restore.feature000064400000007267152265520610013376 0ustar00@mod @mod_lti @core_backup @javascript Feature: Restoring Moodle 2 backup restores LTI configuration Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And the following config values are set as admin: | enableasyncbackup | 0 | Scenario: Backup and restore course with preconfigured site LTI tool on the same site Given the following "mod_lti > tool types" exist: | name | description | baseurl | coursevisible | state | | My site tool | Site tool description | https://www.moodle.org | 2 | 1 | And the following "mod_lti > tool instances" exist: | name | tool | course | | My LTI module | My site tool | C1 | And I am on the "Course 1" course page logged in as admin And I should see "My LTI module" When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: And I am on site homepage And I follow "Course 1 copy 1" Then I should see "My LTI module" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration And "This tool is being used 2 times" "text" should exist in the "//div[contains(@id,'tool-card-container') and contains(., 'My site tool')]" "xpath_element" @javascript Scenario: Backup and restore course with preconfigured course LTI tool on the same site Given the following "mod_lti > course tools" exist: | name | description | baseurl | course | lti_resourcekey | lti_password | lti_launchcontainer | | My course tool | Example description | http://www.example.com/lti/provider.php | C1 | my key | my secret | 5 | # In the first course create an LTI module that uses a course preconfigured tool And the following "mod_lti > tool instances" exist: | name | tool | course | | Test tool activity 2 | My course tool | C1 | And I am on the "Course 1" course page logged in as admin # Backup course and restore into another course And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | When I restore "test_backup.mbz" backup into "Course 2" course using this options: # Make sure the copy of the preconfigured tool was created in the second course with both encrypted and non-encrypted properties. And I am on "Course 2" course homepage with editing mode on And I open "Test tool activity 2" actions menu And I choose "Edit settings" in the open action menu And the field "Activity name" matches value "Test tool activity 2" And I am on "Course 1" course homepage And I navigate to "LTI External tools" in current page administration Then I should see "My course tool" And I open the action menu in "My course tool" "table_row" And I choose "Edit" in the open action menu And the field "Tool URL" matches value "http://www.example.com/lti/provider.php" And the field "Consumer key" matches value "my key" And the field "Shared secret" matches value "my secret" And the field "Default launch container" matches value "Existing window" tests/behat/toolconfigure.feature000064400000002440152265520610013231 0ustar00@mod @mod_lti Feature: Configure tool types In order to allow teachers to add external LTI tools As an admin I need to be able to add, remove and configure tool types Background: Given I log in as "admin" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration @javascript Scenario: Add a tool type from a cartridge URL When I set the field "url" to local url "/mod/lti/tests/fixtures/ims_cartridge_basic_lti_link.xml" And I press "Add Legacy LTI" Then I should see "Enter your consumer key and shared secret" And I press "Save changes" And I should see "Example tool" @javascript Scenario: Try to add a non-existant cartridge When I set the field "url" to local url "/mod/lti/tests/fixtures/nonexistant.xml" And I press "Add Legacy LTI" Then I should see "Enter your consumer key and shared secret" And I press "Save changes" And I should see "Failed to create new tool. Please check the URL and try again." @javascript Scenario: Attempt to add a tool type from a configuration URL, then cancel When I set the field "url" to local url "/mod/lti/tests/fixtures/tool_provider.php" And I press "Add Legacy LTI" Then I should see "Cancel" And I press "cancel-external-registration" tests/behat/addtool.feature000064400000021422152265520610012001 0ustar00@mod @mod_lti Feature: Add tools In order to provide activities for learners As a teacher I need to be able to add instances of external tools to a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | # A site tool configured to show as a preconfigured tool and in the activity chooser. And the following "mod_lti > tool types" exist: | name | baseurl | coursevisible | state | | Teaching Tool 1 | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | # A course tool in course 1. And the following "mod_lti > course tools" exist: | name | baseurl | course | | Course tool 1 | /mod/lti/tests/fixtures/tool_provider.php | C1 | @javascript Scenario: Add a site tool via the activity picker Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I add a "Teaching Tool 1" to section "1" using the activity chooser And I set the field "Activity name" to "Test tool activity 1" And "Launch container" "field" should not be visible # For tool that does not support Content-Item message type, the Select content button must be disabled. And "Select content" "button" should not be visible And "Tool URL" "field" should not be visible And I press "Save and return to course" And I am on the "Test tool activity 1" "lti activity editing" page Then the field "Activity name" matches value "Test tool activity 1" And "Launch container" "field" should not be visible And "Select content" "button" should not be visible And "Tool URL" "field" should not be visible @javascript Scenario: Add a course tool via the activity picker Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I add a "Course tool 1" to section "1" using the activity chooser And I set the field "Activity name" to "Test tool activity 2" And "Launch container" "field" should not be visible # For tool that does not support Content-Item message type, the Select content button must be disabled. And "Select content" "button" should not be visible And I press "Save and return to course" And I am on the "Test tool activity 2" "lti activity editing" page Then the field "Activity name" matches value "Test tool activity 2" And "Launch container" "field" should not be visible And "Select content" "button" should not be visible And "Tool URL" "field" should not be visible @javascript Scenario: Editing a (deprecated) manually configured activity instance, confirming that config changes aren't possible Given the following "activities" exist: | activity | name | course | toolurl | | lti | A manual tool | C1 | /mod/lti/tests/fixtures/ims_cartridge_basic_lti_link.xml | # Add a course tool with the same URL as that of the manually configured instance (the tool URL found in the above cartridge). # This would normally be domain-matched during edit, resulting in the assignment of a preconfigured tool to the instance. # In this case, because config changes and domain matching are disabled, the test confirms this doesn't take place. And the following "mod_lti > course tools" exist: | name | baseurl | course | lti_sendname | lti_sendemailaddr | lti_acceptgrades | | Course tool 2 | http://www.example.com/lti/provider.php | C1 | 0 | 1 | 2 | When I am on the "A manual tool" "lti activity editing" page logged in as teacher1 Then I should see "Manually configured External tool activities are no longer supported" And I follow "Show more..." And I expand all fieldsets # The privacy values below represent the existing values of the privacy settings, before saving and inheriting from the # domain-matched tool values. And the following fields match these values: | Activity name | A manual tool | | id_showdescription | 0 | | Consumer key | 12345 | | Icon URL | http://download.moodle.org/unittest/test.jpg | | Secure icon URL | https://download.moodle.org/unittest/test.jpg | | Tool URL | http://www.example.com/lti/provider.php | | id_instructorchoicesendname | 1 | | id_instructorchoicesendemailaddr | 1 | | id_instructorchoiceacceptgrades | 1 | And the "Activity name" "field" should be enabled And the "Activity description" "field" should be enabled And the "id_showdescription" "checkbox" should be enabled And the "id_showtitlelaunch" "checkbox" should be enabled And the "id_showdescriptionlaunch" "checkbox" should be enabled And the "Secure tool URL" "field" should be disabled And the "Consumer key" "field" should be enabled And the "Shared secret" "field" should be enabled And I click on "Reveal" "icon" And I should see "secret" And the "Custom parameters" "field" should be disabled And the "Icon URL" "field" should be disabled And the "Secure icon URL" "field" should be disabled And I should see "Automatic, based on tool URL" And the "Select content" "button" should be disabled And the "Tool URL" "field" should be disabled And the "id_instructorchoicesendname" "checkbox" should be disabled And the "id_instructorchoicesendemailaddr" "checkbox" should be disabled And the "id_instructorchoiceacceptgrades" "checkbox" should be disabled And I set the following fields to these values: | Activity name | A manual tool name edited | | id_showdescription | 1 | | Consumer key | key | | Shared secret | secret | And I press "Save and return to course" And I am on the "A manual tool" "lti activity editing" page logged in as teacher1 And I follow "Show more..." # This confirms that the instance config, while locked to user edits, still inherits privacy settings from the tool which # it was domain-matched to. And the following fields match these values: | Activity name | A manual tool name edited | | id_showdescription | 1 | | Consumer key | key | | Shared secret | secret | | Icon URL | http://download.moodle.org/unittest/test.jpg | | Secure icon URL | https://download.moodle.org/unittest/test.jpg | | Tool URL | http://www.example.com/lti/provider.php | | id_instructorchoicesendname | 0 | | id_instructorchoicesendemailaddr | 1 | | id_instructorchoiceacceptgrades | 2 | And the "Activity name" "field" should be enabled And the "Activity description" "field" should be enabled And the "id_showdescription" "checkbox" should be enabled And the "id_showtitlelaunch" "checkbox" should be enabled And the "id_showdescriptionlaunch" "checkbox" should be enabled And the "Secure tool URL" "field" should be disabled And the "Consumer key" "field" should be enabled And the "Shared secret" "field" should be enabled And I click on "Reveal" "icon" And I should see "secret" And the "Custom parameters" "field" should be disabled And the "Icon URL" "field" should be disabled And the "Secure icon URL" "field" should be disabled And I should see "Automatic, based on tool URL" And the "Select content" "button" should be disabled And the "Tool URL" "field" should be disabled And the "id_instructorchoicesendname" "checkbox" should be disabled And the "id_instructorchoicesendemailaddr" "checkbox" should be disabled And the "id_instructorchoiceacceptgrades" "checkbox" should be disabled tests/behat/siteadmin_managetool_breadcrumbs.feature000064400000005123152265520610017107 0ustar00@mod @mod_lti Feature: Verify the breadcrumbs in manage tools site administration pages Whenever I navigate to manage tools page in site administration As an admin The breadcrumbs should be visible Background: Given I log in as "admin" Scenario: Verify the breadcrumbs in manage tools page as an admin Given I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration And "Manage tools" "text" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" When I click on "configure a tool manually" "link" Then "External tool configuration" "text" should exist in the ".breadcrumb" "css_element" And "Manage tools" "link" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" And I press "Cancel" And I click on "Manage preconfigured tools" "link" And "Manage preconfigured tools" "text" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" And I click on "Add preconfigured tool" "link" And "External tool configuration" "text" should exist in the ".breadcrumb" "css_element" And "Manage tools" "link" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" And I press "Cancel" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration And I click on "Manage external tool registrations" "link" And "Manage external tool registrations" "text" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" And I click on "Configure a new external tool registration" "link" And "Edit preconfigured tool" "text" should exist in the ".breadcrumb" "css_element" And "Manage external tool registrations" "link" should exist in the ".breadcrumb" "css_element" And "External tool" "link" should exist in the ".breadcrumb" "css_element" And "Activity modules" "link" should exist in the ".breadcrumb" "css_element" tests/behat/lti_completion_pass_grade.feature000064400000007371152265520610015573 0ustar00@mod @mod_lti @core_completion Feature: Pass grade activity completion information in the LTI activity Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | student2 | Vinnie | Student2 | student2@example.com | | student3 | Vinnie | Student3 | student3@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | showcompletionconditions | | Course 1 | C1 | 0 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | course | gradepass | completion | completionview | completionusegrade | completionpassgrade | | lti | Music history | C1 | 50 | 2 | 1 | 1 | 1 | Scenario: Verify that the LTI completion conditions are displayed to teachers Given I am on the "Music history" "lti activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition @javascript Scenario: Verify that students can complete an LTI activity by achieving a passing grade Given I am on the "Music history" "lti activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "90.00" to the user "Vinnie Student1" for the grade item "Music history" And I give the grade "20.00" to the user "Vinnie Student2" for the grade item "Music history" And I press "Save changes" When I am on the "Music history" "lti activity" page logged in as student1 Then the "Receive a grade" completion condition of "Music history" is displayed as "done" Then the "Receive a passing grade" completion condition of "Music history" is displayed as "done" And the "View" completion condition of "Music history" is displayed as "done" When I am on the "Music history" "lti activity" page logged in as student2 Then the "Receive a grade" completion condition of "Music history" is displayed as "done" Then the "Receive a passing grade" completion condition of "Music history" is displayed as "failed" And the "View" completion condition of "Music history" is displayed as "done" When I am on the "Music history" "lti activity" page logged in as student3 Then the "Receive a grade" completion condition of "Music history" is displayed as "todo" Then the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And the "View" completion condition of "Music history" is displayed as "done" And I am on the "Course 1" course page logged in as teacher1 And "Vinnie Student1" user has completed "Music history" activity And "Vinnie Student2" user has completed "Music history" activity And "Vinnie Student3" user has not completed "Music history" activity tests/behat/managecoursetools.feature000064400000042154152265520610014112 0ustar00@mod @mod_lti Feature: Manage course tools In order to provide richer experiences for learners As a teacher I need to be able to add external tools to a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "course" exists: | fullname | Course 1 | | shortname | C1 | | category | 0 | | format | topics | | numsections | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following config values are set as admin: | enableasyncbackup | 0 | Scenario: Create a course tool from the zero state Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "LTI External tools" in current page administration And I should see "LTI External tools are add-on apps" And I should see "There are no LTI External tools yet." When I click on "Add tool" "link" And I press "Cancel" Then I should see "LTI External tools are add-on apps" And I should see "There are no LTI External tools yet." And I click on "Add tool" "link" And I set the following fields to these values: | Tool name | Teaching Tool 1 | | Tool URL | http://example.com | | Tool description | A short description of the tool | And I press "Save changes" And I should see "Teaching Tool 1 added" And I should see "A short description of the tool" in the "Teaching Tool 1" "table_row" Scenario: Viewing a site level tool in the course tools table # The first tool isn't visible in courses, the next two are, and the last tool is in a pending state and is not visible. Given the following "mod_lti > tool types" exist: | name | description | baseurl | coursevisible | state | | Example tool | Another description | https://example.com/tool1 | 0 | 1 | | Test tool 2 | Tool2 description | https://example.com/tool2 | 1 | 1 | | Test tool 3 | Tool3 description | https://example.com/tool3 | 2 | 1 | | Test tool 4 | Tool4 description | https://example.com/tool4 | 2 | 2 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "LTI External tools" in current page administration Then I should see "Test tool 2" in the "reportbuilder-table" "table" And "You don't have permission to edit this tool" "icon" should exist in the "Test tool 2" "table_row" And I should see "Test tool 3" in the "reportbuilder-table" "table" And "You don't have permission to edit this tool" "icon" should exist in the "Test tool 3" "table_row" And I should not see "Example tool" in the "reportbuilder-table" "table" And I should not see "Test tool 4" in the "reportbuilder-table" "table" Scenario: Viewing course tools without the capability to add/edit but having the capability to use Given the following "role capability" exists: | role | editingteacher | | mod/lti:addcoursetool | prohibit | | mod/lti:addpreconfiguredinstance | allow | And the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Test tool | Example description | https://example.com/tool | C1 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "LTI External tools" in current page administration Then "You don't have permission to edit this tool" "icon" should exist in the "Test tool" "table_row" Scenario: Viewing course tools with the capability to add/edit and without the capability to use Given the following "role capability" exists: | role | editingteacher | | mod/lti:addcoursetool | allow | | mod/lti:addpreconfiguredinstance | prohibit | And the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Test tool | Example description | https://example.com/tool | C1 | When I am on the "Course 1" course page logged in as teacher1 Then "LTI External tools" "link" should not exist in current page administration @javascript Scenario: Edit a course tool Given the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Test tool | Example description | https://example.com/tool | C1 | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "LTI External tools" in current page administration And the "Edit" item should exist in the "Actions" action menu of the "Test tool" "table_row" And the "Delete" item should exist in the "Actions" action menu of the "Test tool" "table_row" When I open the action menu in "Test tool" "table_row" And I choose "Edit" in the open action menu And I press "Cancel" Then I should see "Test tool" in the "reportbuilder-table" "table" And I open the action menu in "Test tool" "table_row" And I choose "Edit" in the open action menu And I set the following fields to these values: | Tool name | Test tool (edited) | | Tool URL | http://example.com | | Tool description | A short description of the tool (edited) | And I press "Save changes" And I should see "Changes saved" And I should see "A short description of the tool (edited)" in the "Test tool (edited)" "table_row" @javascript Scenario: Navigate through the listing of course tools Given 20 "mod_lti > course tools" exist with the following data: | name | Test tool [count] | | description | Example description [count] | | baseurl | https://www.example.com/tool[count] | | course | C1 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "LTI External tools" in current page administration Then I should see "Test tool 1" in the "reportbuilder-table" "table" And I click on "Name" "link" And I should see "Test tool 20" in the "reportbuilder-table" "table" And I click on "2" "link" in the "page" "region" And I should see "Test tool 1" in the "reportbuilder-table" "table" @javascript Scenario: Delete a course tool Given the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Test tool | Example description | https://example.com/tool | C1 | | Another tool | Example 123 | https://another.example.com/tool | C1 | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "LTI External tools" in current page administration When I open the action menu in "Test tool" "table_row" And I choose "Delete" in the open action menu Then I should see "This will delete Test tool from the available LTI tools in your course." And I click on "Cancel" "button" in the "Delete Test tool" "dialogue" And I should see "Test tool" in the "reportbuilder-table" "table" And I open the action menu in "Test tool" "table_row" And I choose "Delete" in the open action menu And I should see "This will delete Test tool from the available LTI tools in your course." And I click on "Delete" "button" in the "Delete Test tool" "dialogue" And I should see "Test tool deleted" And I should not see "Test tool" in the "reportbuilder-table" "table" @javascript Scenario: Add a course tool using a cartridge URL Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "LTI External tools" in current page administration When I click on "Add tool" "link" And I set the following fields to these values: | Tool name | Test tool 1 | | Tool description | Test tool 1 description | And I set the field "Tool URL" to local url "/mod/lti/tests/fixtures/ims_cartridge_basic_lti_link.xml" And I press "Save changes" Then I should see "Test tool 1" in the "reportbuilder-table" "table" # The cartridge description, if set, overrides the description set in the type edit form (bug?). And I should see "Example tool description" in the "Test tool 1" "table_row" And I open the action menu in "Test tool 1" "table_row" And I choose "Edit" in the open action menu And the field "Tool name" matches value "Test tool 1" And the field "Tool URL" matches value "http://www.example.com/lti/provider.php" And the field "Icon URL" matches value "http://download.moodle.org/unittest/test.jpg" And the field "Secure icon URL" matches value "https://download.moodle.org/unittest/test.jpg" @javascript Scenario: Site tool appearing in activity chooser according to settings Given the following "mod_lti > tool types" exist: | name | baseurl | coursevisible | state | | Teaching Tool 1 | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | | Teaching Tool 2 | /mod/lti/tests/fixtures/tool_provider.php | 1 | 1 | | Teaching Tool 3 | /mod/lti/tests/fixtures/tool_provider.php | 0 | 1 | And the following "courses" exist: | fullname | shortname | category | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C2 | editingteacher | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I open the activity chooser And I should see "Teaching Tool 1" in the ".modal-body" "css_element" And I should not see "Teaching Tool 2" in the ".modal-body" "css_element" And I should not see "Teaching Tool 3" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" And I navigate to "LTI External tools" in current page administration And I should not see "Teaching Tool 3" And I click on "Don't show in activity chooser" "field" in the "Teaching Tool 1" "table_row" And I click on "Show in activity chooser" "field" in the "Teaching Tool 2" "table_row" And I am on "Course 1" course homepage And I open the activity chooser And I should not see "Teaching Tool 1" in the ".modal-body" "css_element" And I should see "Teaching Tool 2" in the ".modal-body" "css_element" And I should not see "Teaching Tool 3" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" # Should not affect other courses. And I am on "Course 2" course homepage And I open the activity chooser And I should see "Teaching Tool 1" in the ".modal-body" "css_element" And I should not see "Teaching Tool 2" in the ".modal-body" "css_element" And I should not see "Teaching Tool 3" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" And I am on "Course 1" course homepage And I navigate to "LTI External tools" in current page administration And I click on "Show in activity chooser" "field" in the "Teaching Tool 1" "table_row" And I click on "Don't show in activity chooser" "field" in the "Teaching Tool 2" "table_row" And I am on "Course 1" course homepage And I open the activity chooser And I should see "Teaching Tool 1" in the ".modal-body" "css_element" And I should not see "Teaching Tool 2" in the ".modal-body" "css_element" And I should not see "Teaching Tool 3" in the ".modal-body" "css_element" When the following "role capability" exists: | role | editingteacher | | mod/lti:addcoursetool | prohibit | And I am on "Course 1" course homepage with editing mode on And I navigate to "LTI External tools" in current page administration Then the "Don't show in activity chooser" "field" should be disabled And the "Show in activity chooser" "field" should be disabled @javascript Scenario: Course tool appearing in activity chooser according to settings Given the following "mod_lti > course tools" exist: | name | baseurl | course | coursevisible | | Course Tool 1 | /mod/lti/tests/fixtures/tool_provider.php | C1 | 2 | | Course Tool 2 | /mod/lti/tests/fixtures/tool_provider.php | C1 | 1 | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I open the activity chooser And I should see "Course Tool 1" in the ".modal-body" "css_element" And I should not see "Course Tool 2" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" And I navigate to "LTI External tools" in current page administration And I click on "Don't show in activity chooser" "field" in the "Course Tool 1" "table_row" And I click on "Show in activity chooser" "field" in the "Course Tool 2" "table_row" And I am on "Course 1" course homepage And I open the activity chooser And I should not see "Course Tool 1" in the ".modal-body" "css_element" And I should see "Course Tool 2" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" And I navigate to "LTI External tools" in current page administration And I click on "Show in activity chooser" "field" in the "Course Tool 1" "table_row" And I click on "Don't show in activity chooser" "field" in the "Course Tool 2" "table_row" And I am on "Course 1" course homepage And I open the activity chooser And I should see "Course Tool 1" in the ".modal-body" "css_element" And I should not see "Course Tool 2" in the ".modal-body" "css_element" When the following "role capability" exists: | role | editingteacher | | mod/lti:addcoursetool | prohibit | And I am on "Course 1" course homepage with editing mode on And I navigate to "LTI External tools" in current page administration Then the "Don't show in activity chooser" "field" should be disabled And the "Show in activity chooser" "field" should be disabled @javascript Scenario: Site and course tools settings are preserved when backup and restore Given the following "mod_lti > tool types" exist: | name | baseurl | coursevisible | state | | Teaching Tool 1 | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | | Teaching Tool 2 | /mod/lti/tests/fixtures/tool_provider.php | 1 | 1 | And the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Course Tool 1 | Example description | https://example.com/tool | C1 | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I add a "Teaching Tool 1" to section "1" using the activity chooser And I set the field "Activity name" to "Test tool activity 1" And I press "Save and return to course" And I add a "Course Tool 1" to section "1" using the activity chooser And I set the field "Activity name" to "Course tool activity 1" And I press "Save and return to course" And I navigate to "LTI External tools" in current page administration And I click on "Don't show in activity chooser" "field" in the "Teaching Tool 1" "table_row" And I click on "Show in activity chooser" "field" in the "Teaching Tool 2" "table_row" And I click on "Don't show in activity chooser" "field" in the "Course Tool 1" "table_row" And I am on "Course 1" course homepage And I add a "Teaching Tool 2" to section "1" using the activity chooser And I set the field "Activity name" to "Test tool activity 2" And I press "Save and return to course" When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Restored course | And I should see "Restored course" And I open the activity chooser Then I should not see "Teaching Tool 1" in the ".modal-body" "css_element" And I should see "Teaching Tool 2" in the ".modal-body" "css_element" And I should not see "Course Tool 2" in the ".modal-body" "css_element" And I click on "Close" "button" in the ".modal-dialog" "css_element" And I navigate to "LTI External tools" in current page administration And I should see "Show in activity chooser" in the "Teaching Tool 1" "table_row" And I should see "Don't show in activity chooser" in the "Teaching Tool 2" "table_row" And I should see "Show in activity chooser" in the "Course Tool 1" "table_row" tests/behat/contentitemregistration.feature000064400000003155152265520610015342 0ustar00@mod @mod_lti Feature: Create/edit tool configuration that has Deep Linking support In order to provide external tools that support Deep Linking for teachers and learners As an admin I need to be able to configure external tool registrations that support Deep Linking. Background: Given I log in as "admin" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration Scenario: Verifying ContentItemSelectionRequest selection support in external tool registration When I follow "Manage external tool registrations" And I follow "Configure a new external tool registration" Then I should see "ContentItemSelectionRequest" in the "Capabilities" "select" @javascript Scenario: Creating and editing tool configuration that has Content-Item support When I follow "configure a tool manually" And I set the field "Tool name" to "Test tool" And I set the field "Tool URL" to local url "/mod/lti/tests/fixtures/tool_provider.php" And I set the field "Tool configuration usage" to "Show in activity chooser and as a preconfigured tool" And I expand all fieldsets And I set the field "Supports Deep Linking (Content-Item Message)" to "1" And I press "Save changes" And I follow "Edit" And I expand all fieldsets Then the field "Supports Deep Linking (Content-Item Message)" matches value "1" And I set the field "Supports Deep Linking (Content-Item Message)" to "0" And I press "Save changes" And I follow "Edit" And I expand all fieldsets And the field "Supports Deep Linking (Content-Item Message)" matches value "0" tests/behat/restricttocategory.feature000064400000013172152265520610014316 0ustar00@mod @mod_lti Feature: Make an LTI only available to specific course categories In order to restrict which courses a tool can be used in As an administrator I need to be able to select which course category the tool is available in Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "categories" exist: | name | category | idnumber | | cata | 0 | cata | | catca | cata | catca | | catb | 0 | catb | | catcb | catb | catcb | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | cata | | Course 2 | C2 | catb | | Course 3 | C3 | catca | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | teacher1 | C3 | editingteacher | And the following "mod_lti > tool types" exist: | name | description | baseurl | coursevisible | state | lti_coursecategories | | Teaching Tool 1 | Tool 1 description | /mod/lti/tests/fixtures/tool_provider.php | 1 | 1 | catb | | Teaching Tool 2 | Tool 2 description | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | catca | Scenario: Tool is set to "Show as preconfigured tool when adding an external tool" on parent category Given I am on the "Course 2" course page logged in as teacher1 When I navigate to "LTI External tools" in current page administration Then I should see "Teaching Tool 1" in the "reportbuilder-table" "table" And I should not see "Teaching Tool 2" in the "reportbuilder-table" "table" @javascript Scenario: Tool is set to "Show in activity chooser and as preconfigured tool" on child category Given I log in as "teacher1" When I am on "Course 3" course homepage with editing mode on And I open the activity chooser Then I should see "Teaching Tool 2" in the "Add an activity or resource" "dialogue" And I should not see "Teaching Tool 1" in the "Add an activity or resource" "dialogue" @javascript Scenario: View a course in a category in which no tools are available Given I log in as "teacher1" When I am on "Course 1" course homepage with editing mode on And I open the activity chooser Then I should not see "Teaching Tool 1" in the "Add an activity or resource" "dialogue" And I should not see "Teaching Tool 2" in the "Add an activity or resource" "dialogue" @javascript Scenario: Editing and saving selected parent / child categories Given I log in as "admin" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration And I follow "Manage preconfigured tools" And I follow "Add preconfigured tool" And I expand all fieldsets And I click on "catb" "link" And I set the following fields to these values: | Tool name | Teaching Tool 3 | | Tool configuration usage | Do not show; use only when a matching tool URL is entered | | catb | 1 | # If parent is selected, child should be selected. And the field "catcb" matches value "1" # If parent is unselected, child should be unselected. And I set the following fields to these values: | catb | 0 | And the field "catcb" matches value "0" # If parent is selected, child is unselected, parent should still be selected. # Step 1 - Select parent first so child is selected. And I set the following fields to these values: | catb | 1 | And the field "catcb" matches value "1" # Step 2 - Unselect child but parent should stay as selected. And I set the following fields to these values: | catcb | 0 | And the field "catb" matches value "1" And I set the field "Tool URL" to local url "/mod/lti/tests/fixtures/tool_provider.php" And I press "Save changes" And I wait until the page is ready And I should see "Teaching Tool 3" When I click on "Update" "link" in the "Teaching Tool 3" "table_row" And I expand all fieldsets Then the following fields match these values: | catb | 1 | | catcb | 0 | @javascript Scenario: Category restriction only shown for a site tool Given the following "mod_lti > tool types" exist: | name | baseurl | coursevisible | state | | Teaching Tool 1 | /mod/lti/tests/fixtures/tool_provider.php | 2 | 1 | And the following "mod_lti > course tools" exist: | name | description | baseurl | course | | Course Tool 1 | Example description | https://example.com/tool | C1 | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I navigate to "LTI External tools" in current page administration When I click on "Add tool" "link" And I should not see "Restrict to category" And I press "Cancel" And I open the action menu in "Course Tool 1" "table_row" And I choose "Edit" in the open action menu And I should not see "Restrict to category" And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration And I follow "Manage preconfigured tools" And I follow "Add preconfigured tool" And I should see "Restrict to category" And I press "Cancel" And I click on "Update" "link" in the "Teaching Tool 1" "table_row" Then I should see "Restrict to category" tests/mod_lti_edit_types_form_test.php000064400000010542152265520610014367 0ustar00. /** * Unit tests for mod_lti edit_form * * @package mod_lti * @copyright 2023 Jackson D'Souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/course_categories_trait.php'); /** * Unit tests for mod_lti edit_form * * @package mod_lti * @copyright 2023 Jackson D'Souza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 */ final class mod_lti_edit_types_form_test extends \advanced_testcase { // There are shared helpers for these tests in the helper course_categories_trait. use \mod_lti_course_categories_trait; /** * Tests the nested course categories JSON returned by public method mod_lti_edit_types_form::lti_build_category_tree(). * * @covers \mod_lti_edit_types_form::lti_build_category_tree */ public function test_set_nested_categories(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/lti/tests/fixtures/test_edit_form.php'); $this->resetAfterTest(true); $this->setAdminUser(); $ltiform = new test_edit_form(null); $ltiform->definition_after_data(); // Setup fixture. $coursecategories = $this->setup_course_categories(); $categoryarray[] = [ "id" => $coursecategories['topcat']->id, "parent" => $coursecategories['topcat']->parent, "name" => $coursecategories['topcat']->name, "nodes" => [ [ "id" => $coursecategories['subcata']->id, "parent" => $coursecategories['topcat']->id, "name" => $coursecategories['subcata']->name, "nodes" => [ [ "id" => $coursecategories['subcatca']->id, "parent" => $coursecategories['subcata']->id, "name" => $coursecategories['subcatca']->name, "nodes" => "", "haschildren" => "" ] ], "haschildren" => "1" ], [ "id" => $coursecategories['subcatb']->id, "parent" => $coursecategories['topcat']->id, "name" => $coursecategories['subcatb']->name, "nodes" => [ [ "id" => $coursecategories['subcatcb']->id, "parent" => $coursecategories['subcatb']->id, "name" => $coursecategories['subcatcb']->name, "nodes" => "", "haschildren" => "" ] ], "haschildren" => "1" ] ], "haschildren" => "1" ]; $records = $DB->get_records('course_categories', [], 'sortorder, id', 'id, parent, name'); $allcategories = json_decode(json_encode($records), true); $coursecategoriesarray = $ltiform->lti_build_category_tree($allcategories); $this->assertEquals($categoryarray, $coursecategoriesarray); } } externalregistrationreturn.php000064400000004502152265520610012777 0ustar00. /** * Handle the return from the Tool Provider after registering a tool proxy. * * @package mod_lti * @copyright 2015 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $status = optional_param('status', '', PARAM_TEXT); $msg = optional_param('lti_msg', '', PARAM_TEXT); $err = optional_param('lti_errormsg', '', PARAM_TEXT); $id = optional_param('id', 0, PARAM_INT); // No guest autologin. require_sesskey(); require_login(0, false); $systemcontext = context_system::instance(); require_capability('moodle/site:config', $systemcontext); $pageurl = new moodle_url('/mod/lti/externalregistrationreturn.php'); $PAGE->set_context($systemcontext); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('maintenance'); $output = $PAGE->get_renderer('mod_lti'); echo $output->header(); // Check status and lti_errormsg. if ($status !== 'success' && empty($err)) { // We have a failed status and an empty lti_errormsg. Check if we can use lti_msg. if (!empty($msg)) { // The lti_msg attribute is set, use this as the error message. $err = $msg; } else { // Otherwise, use our generic error message. $err = get_string('failedtocreatetooltype', 'mod_lti'); } } $params = array('message' => s($msg), 'error' => s($err), 'id' => $id, 'status' => s($status)); $page = new \mod_lti\output\external_registration_return_page(); echo $output->render($page); $PAGE->requires->js_call_amd('mod_lti/external_registration_return', 'init', $params); echo $output->footer(); certs.php000064400000002247152265520610006406 0ustar00. /** * This file returns an array of available public keys * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_lti\local\ltiopenid\jwks_helper; define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . '/../../config.php'); @header('Content-Type: application/json; charset=utf-8'); echo json_encode(jwks_helper::get_jwks(), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); startltiadvregistration.php000064400000005720152265520610012261 0ustar00. /** * Redirect the user to registration with token and openid config url as query params. * * @package mod_lti * @copyright 2020 Cengage * @author Claude Vervoort * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use Firebase\JWT\JWT; use mod_lti\local\ltiopenid\jwks_helper; use mod_lti\local\ltiopenid\registration_helper; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/weblib.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_login(); $context = context_system::instance(); require_capability('moodle/site:config', $context); $starturl = required_param('url', PARAM_URL); $typeid = optional_param('type', -1, PARAM_INT); $types = lti_get_tools_by_url($starturl, null); if (!empty($types) && $typeid == -1) { // There are matching types for the registration domain, let's prompt the user to upgrade. $pageurl = new moodle_url('/mod/lti/startltiadvregistration.php'); $PAGE->set_context($context); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('maintenance'); $output = $PAGE->get_renderer('mod_lti'); $page = new \mod_lti\output\registration_upgrade_choice_page($types, $starturl); echo $output->header(); echo $output->render($page); echo $output->footer(); } else { // Let's actually start the registration process by launching the tool registration // endpoint with the registration token and the site config url. require_sesskey(); $sub = registration_helper::get()->new_clientid(); $scope = registration_helper::REG_TOKEN_OP_NEW_REG; if ($typeid > 0) { // In the context of an update, the sub is the id of the type. $sub = strval($typeid); $scope = registration_helper::REG_TOKEN_OP_UPDATE_REG; } $now = time(); $token = [ "sub" => $sub, "scope" => $scope, "iat" => $now, "exp" => $now + HOURSECS ]; $privatekey = jwks_helper::get_private_key(); $regtoken = JWT::encode($token, $privatekey['key'], 'RS256', $privatekey['kid']); $confurl = new moodle_url('/mod/lti/openid-configuration.php'); $url = new moodle_url($starturl); $url->param('openid_configuration', $confurl->out(false)); $url->param('registration_token', $regtoken); header("Location: ".$url->out(false)); } deprecatedlib.php000064400000002173152265520610010053 0ustar00. /** * List of deprecated mod_lti functions. * * @package mod_lti * @copyright 2022 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * @deprecated since Moodle 4.3 */ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.3', mdl: 'MDL-76953', final: true)] function lti_libxml_disable_entity_loader() { \core\deprecation::emit_deprecation(__FUNCTION__); } upgradelib.php000064400000004116152265520610007401 0ustar00. /** * This file contains functions used by upgrade and install. * * Because this is used during install it should not include additional files. * * @package mod_lti * @copyright 2019 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * This function checks if a private key has been generated for this site. * * If the key does not exist it generates a new one. If the openssl * extension is not installed or configured properly it returns a warning message. * * @return string A warning message if a private key does not exist and cannot be generated. */ function mod_lti_verify_private_key() { $key = get_config('mod_lti', 'privatekey'); // If we already generated a valid key, no need to check. if (empty($key)) { // Create the private key. $kid = bin2hex(openssl_random_pseudo_bytes(10)); set_config('kid', $kid, 'mod_lti'); $config = array( "digest_alg" => "sha256", "private_key_bits" => 2048, "private_key_type" => OPENSSL_KEYTYPE_RSA, ); $res = openssl_pkey_new($config); openssl_pkey_export($res, $privatekey); if (!empty($privatekey)) { set_config('privatekey', $privatekey, 'mod_lti'); } else { return get_string('opensslconfiginvalid', 'mod_lti'); } } return ''; } return.php000064400000011206152265520610006600 0ustar00. /** * Handle the return back to Moodle from the tool provider * * @package mod_lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Chris Scribner */ require_once('../../config.php'); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $courseid = required_param('course', PARAM_INT); $instanceid = optional_param('instanceid', 0, PARAM_INT); $errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); $msg = optional_param('lti_msg', '', PARAM_TEXT); $unsigned = optional_param('unsigned', '0', PARAM_INT); $launchcontainer = optional_param('launch_container', LTI_LAUNCH_CONTAINER_WINDOW, PARAM_INT); $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $lti = null; $context = null; if (!empty($instanceid)) { $lti = $DB->get_record('lti', array('id' => $instanceid), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('lti', $lti->id, $lti->course, false, MUST_EXIST); $context = context_module::instance($cm->id); } require_login($course); require_sesskey(); if (!empty($errormsg) || !empty($msg)) { $url = new moodle_url('/mod/lti/return.php', array('course' => $courseid)); $PAGE->set_url($url); $pagetitle = strip_tags($course->shortname); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); // Avoid frame-in-frame action. if ($launchcontainer == LTI_LAUNCH_CONTAINER_EMBED || $launchcontainer == LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS) { $PAGE->set_pagelayout('embedded'); } else { $PAGE->set_pagelayout('incourse'); } echo $OUTPUT->header(); if (!empty($lti) and !empty($context)) { echo $OUTPUT->heading(format_string($lti->name, true, array('context' => $context))); } } if (!empty($errormsg)) { echo get_string('lti_launch_error', 'lti'); p($errormsg); if ($unsigned == 1) { $contextcourse = context_course::instance($courseid); echo '

'; $links = new stdClass(); if (has_capability('mod/lti:addcoursetool', $contextcourse)) { $coursetooleditor = new moodle_url('mod/lti/coursetools.php', ['id' => $courseid]); $links->course_tool_editor = $coursetooleditor->out(false); echo get_string('lti_launch_error_unsigned_help', 'lti', $links); } if (!empty($lti) && has_capability('mod/lti:requesttooladd', $contextcourse)) { $adminrequesturl = new moodle_url('/mod/lti/request_tool.php', array('instanceid' => $lti->id, 'sesskey' => sesskey())); $links->admin_request_url = $adminrequesturl->out(false); echo get_string('lti_launch_error_tool_request', 'lti', $links); } } echo $OUTPUT->footer(); } else if (!empty($msg)) { p($msg); echo $OUTPUT->footer(); } else { $courseurl = new moodle_url('/course/view.php', array('id' => $courseid)); $url = $courseurl->out(); // Avoid frame-in-frame action. if ($launchcontainer == LTI_LAUNCH_CONTAINER_EMBED || $launchcontainer == LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS) { // Output a page containing some script to break out of frames and redirect them. echo ''; $script = " "; $clickhere = get_string('return_to_course', 'lti', (object)array('link' => $url)); $noscript = " "; echo $script; echo $noscript; echo ''; } else { // If no error, take them back to the course. redirect($url); } } launch.php000064400000010501152265520610006530 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains all necessary code to view a lti activity instance * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/lti/lib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $cmid = required_param('id', PARAM_INT); // Course Module ID. $triggerview = optional_param('triggerview', 1, PARAM_BOOL); $action = optional_param('action', '', PARAM_TEXT); $foruserid = optional_param('user', 0, PARAM_INT); $cm = get_coursemodule_from_id('lti', $cmid, 0, false, MUST_EXIST); $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST); $typeid = $lti->typeid; if (empty($typeid) && ($tool = lti_get_tool_by_url_match($lti->toolurl))) { $typeid = $tool->id; } if ($typeid) { $config = lti_get_type_config($typeid); $missingtooltype = empty($config); if (!$missingtooltype) { $config = lti_get_type_type_config($typeid); if ($config->lti_ltiversion === LTI_VERSION_1P3) { if (!isset($SESSION->lti_initiatelogin_status)) { $msgtype = 'basic-lti-launch-request'; if ($action === 'gradeReport') { $msgtype = 'LtiSubmissionReviewRequest'; } echo lti_initiate_login($cm->course, $cmid, $lti, $config, $msgtype, '', '', $foruserid); exit; } else { unset($SESSION->lti_initiatelogin_status); } } } } $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); $context = context_module::instance($cm->id); require_login($course, true, $cm); require_capability('mod/lti:view', $context); if (!empty($missingtooltype)) { $PAGE->set_url(new moodle_url('/mod/lti/launch.php')); $PAGE->set_context($context); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->set_pagelayout('incourse'); echo $OUTPUT->header(); throw new moodle_exception('tooltypenotfounderror', 'mod_lti'); } // Completion and trigger events. if ($triggerview) { lti_view($lti, $course, $cm, $context); } $lti->cmid = $cm->id; lti_launch_tool($lti, $foruserid); register.php000064400000010572152265520610007112 0ustar00. /** * This file contains all necessary code to launch a Tool Proxy registration * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); $id = required_param('id', PARAM_INT); $tab = optional_param('tab', '', PARAM_ALPHAEXT); require_login(0, false); $redirect = new moodle_url('/mod/lti/toolproxies.php', array('tab' => $tab)); $redirect = $redirect->out(); require_sesskey(); $toolproxies = $DB->get_records('lti_tool_proxies'); $duplicate = false; foreach ($toolproxies as $key => $toolproxy) { if (($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_ACCEPTED)) { if ($toolproxy->regurl == $toolproxies[$id]->regurl) { $duplicate = true; break; } } } $redirect = new moodle_url('/mod/lti/toolproxies.php'); if ($duplicate) { redirect($redirect, get_string('duplicateregurl', 'lti')); } $profileservice = lti_get_service_by_name('profile'); if (empty($profileservice)) { redirect($redirect, get_string('noprofileservice', 'lti')); } $url = new moodle_url('/mod/lti/register.php', array('id' => $id)); $PAGE->set_url($url); admin_externalpage_setup('ltitoolproxies'); $PAGE->set_heading(get_string('toolproxyregistration', 'lti')); $PAGE->set_title(get_string('toolproxyregistration', 'lti')); // Print the page header. echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('toolproxyregistration', 'lti')); echo $OUTPUT->box_start('generalbox'); // Request the registration request content with an object tag. $registration = new moodle_url('/mod/lti/registration.php', array('id' => $id, 'sesskey' => sesskey())); echo "

\n"; echo get_string('register_warning', 'lti'); echo "\n

\n"; echo ''; // Output script to make the object tag be as large as possible. $resize = ' '; echo $resize; // Finish the page. echo $OUTPUT->box_end(); echo $OUTPUT->footer(); settings.php000064400000020035152265520610007121 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines the global lti administration form * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /* * @var admin_settingpage $settings */ $modltifolder = new admin_category('modltifolder', new lang_string('pluginname', 'mod_lti'), $module->is_enabled() === false); $ADMIN->add('modsettings', $modltifolder); $settings->visiblename = new lang_string('manage_tools', 'mod_lti'); $settings->hidden = true; $ADMIN->add('modltifolder', $settings); $proxieslink = new admin_externalpage('ltitoolproxies', get_string('manage_tool_proxies', 'lti'), new moodle_url('/mod/lti/toolproxies.php')); $proxieslink->hidden = true; $ADMIN->add('modltifolder', $proxieslink); $ADMIN->add('modltifolder', new admin_externalpage('ltitoolconfigure', get_string('manage_external_tools', 'lti'), new moodle_url('/mod/lti/toolconfigure.php'))); foreach (core_plugin_manager::instance()->get_plugins_of_type('ltisource') as $plugin) { /* * @var \mod_lti\plugininfo\ltisource $plugin */ $plugin->load_settings($ADMIN, 'modltifolder', $hassiteconfig); } $toolproxiesurl = new moodle_url('/mod/lti/toolproxies.php'); $toolproxiesurl = $toolproxiesurl->out(); if ($ADMIN->fulltree) { require_once($CFG->dirroot.'/mod/lti/locallib.php'); $configuredtoolshtml = ''; $pendingtoolshtml = ''; $rejectedtoolshtml = ''; $active = get_string('active', 'lti'); $pending = get_string('pending', 'lti'); $rejected = get_string('rejected', 'lti'); // Gather strings used for labels in the inline JS. $PAGE->requires->strings_for_js( array( 'typename', 'baseurl', 'action', 'createdon' ), 'mod_lti' ); $types = lti_filter_get_types(get_site()->id); $configuredtools = lti_filter_tool_types($types, LTI_TOOL_STATE_CONFIGURED); $configuredtoolshtml = lti_get_tool_table($configuredtools, 'lti_configured'); $pendingtools = lti_filter_tool_types($types, LTI_TOOL_STATE_PENDING); $pendingtoolshtml = lti_get_tool_table($pendingtools, 'lti_pending'); $rejectedtools = lti_filter_tool_types($types, LTI_TOOL_STATE_REJECTED); $rejectedtoolshtml = lti_get_tool_table($rejectedtools, 'lti_rejected'); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $activeselected = ''; $pendingselected = ''; $rejectedselected = ''; switch ($tab) { case 'lti_pending': $pendingselected = 'class="selected"'; break; case 'lti_rejected': $rejectedselected = 'class="selected"'; break; default: $activeselected = 'class="selected"'; break; } $addtype = get_string('addtype', 'lti'); $config = get_string('manage_tool_proxies', 'lti'); $addtypeurl = "{$CFG->wwwroot}/mod/lti/typessettings.php?action=add&sesskey={$USER->sesskey}"; $template = <<< EOD
$configuredtoolshtml
$pendingtoolshtml
$rejectedtoolshtml
EOD; $settings->add(new admin_setting_heading('lti_types', new lang_string('external_tool_types', 'lti') . $OUTPUT->help_icon('main_admin', 'lti'), $template)); } // Tell core we already added the settings structure. $settings = null; OAuthBody.php000064400000012642152265520610007124 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. namespace moodle\mod\lti; // Using a namespace as the basicLTI module imports classes with the same names. defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/mod/lti/OAuth.php'); require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); /** * * @param int $typeid LTI type ID. * @param string[] $scopes Array of scopes which give permission for the current request. * * @return string|int|boolean The OAuth consumer key, the LTI type ID for the validated bearer token, true for requests not requiring a scope, otherwise false. */ function get_oauth_key_from_headers($typeid = null, $scopes = null) { global $DB; $now = time(); $requestheaders = OAuthUtil::get_headers(); if (isset($requestheaders['Authorization'])) { if (substr($requestheaders['Authorization'], 0, 6) == "OAuth ") { $headerparameters = OAuthUtil::split_header($requestheaders['Authorization']); return format_string($headerparameters['oauth_consumer_key']); } else if (empty($scopes)) { return true; } else if (substr($requestheaders['Authorization'], 0, 7) == 'Bearer ') { $tokenvalue = trim(substr($requestheaders['Authorization'], 7)); $conditions = array('token' => $tokenvalue); if (!empty($typeid)) { $conditions['typeid'] = intval($typeid); } $token = $DB->get_record('lti_access_tokens', $conditions); if ($token) { // Log token access. $DB->set_field('lti_access_tokens', 'lastaccess', $now, array('id' => $token->id)); $permittedscopes = json_decode($token->scope); if ((intval($token->validuntil) > $now) && !empty(array_intersect($scopes, $permittedscopes))) { return intval($token->typeid); } } } } return false; } function handle_oauth_body_post($oauthconsumerkey, $oauthconsumersecret, $body, $requestheaders = null) { if ($requestheaders == null) { $requestheaders = OAuthUtil::get_headers(); } // Must reject application/x-www-form-urlencoded. if (isset($requestheaders['Content-type'])) { if ($requestheaders['Content-type'] == 'application/x-www-form-urlencoded' ) { throw new OAuthException("OAuth request body signing must not use application/x-www-form-urlencoded"); } } if (isset($requestheaders['Authorization']) && (substr($requestheaders['Authorization'], 0, 6) == "OAuth ")) { $headerparameters = OAuthUtil::split_header($requestheaders['Authorization']); $oauthbodyhash = $headerparameters['oauth_body_hash']; } if ( ! isset($oauthbodyhash) ) { throw new OAuthException("OAuth request body signing requires oauth_body_hash body"); } // Verify the message signature. $store = new TrivialOAuthDataStore(); $store->add_consumer($oauthconsumerkey, $oauthconsumersecret); $server = new OAuthServer($store); $method = new OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = OAuthRequest::from_request(); try { $server->verify_request($request); } catch (\Exception $e) { $message = $e->getMessage(); throw new OAuthException("OAuth signature failed: " . $message); } $postdata = $body; $hash = base64_encode(sha1($postdata, true)); if ( $hash != $oauthbodyhash ) { throw new OAuthException("OAuth oauth_body_hash mismatch"); } return $postdata; } mod_form.php000064400000072601152265520610007071 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file defines the main lti configuration form * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/course/moodleform_mod.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); class mod_lti_mod_form extends moodleform_mod { /** @var int the tool typeid, or 0 if the instance form is being created for a manually configured tool instance.*/ protected int $typeid; /** @var string|null type */ protected ?string $type; /** * Constructor. * * Throws an exception if trying to init the form for a new manual instance of a tool, which is not supported in 4.3 onward. * * @param \stdClass $current the current form data. * @param string $section the section number. * @param \stdClass $cm the course module object. * @param \stdClass $course the course object. * @throws moodle_exception if trying to init the form for the creation of a manual instance, which is no longer supported. */ public function __construct($current, $section, $cm, $course) { // Setup some of the pieces used to control display in the form definition() method. // Type ID parameter being passed when adding an preconfigured tool from activity chooser. $this->typeid = optional_param('typeid', 0, PARAM_INT); $this->type = optional_param('type', null, PARAM_ALPHA); // Only permit construction if the form deals with editing an existing instance (current->id not empty), or creating an // instance from a preconfigured tool type ($this->typeid not empty). Make an exception for callers, such as core_completion // which aren't instantiating the form with the expected data, by checking whether the page has been set up, which is the // case for normal uses. global $PAGE; if ($PAGE->has_set_url() && str_contains($PAGE->url, '/course/modedit.php')) { if (empty($this->typeid) && empty($current->id)) { throw new moodle_exception('lti:addmanualinstanceprohibitederror', 'mod_lti'); } } parent::__construct($current, $section, $cm, $course); } /** * Defines the form for legacy instances. Here tool config is frozen because the manual configuration method is deprecated. * * @param array $instancetypes the array of options for the legacy 'preconfigured tools' select. * @return void */ protected function legacy_instance_form_definition(array $instancetypes): void { global $OUTPUT; // The legacy form handles instances which are either entirely manually configured (current->typeid = 0), or which are // manually configured and have been domain-matched to a preconfigured tool (current->typeid != 0). $manualinstance = empty($this->current->typeid); $matchestoolnotavailabletocourse = !$manualinstance; $typeid = $manualinstance ? '0' : $this->current->typeid; // Since 'mod/lti:addmanualinstance' capability is deprecated, determining which users may have had access to the certain // form fields (the manual config fields) isn't straightforward. Users without 'mod/lti:addmanualinstance' would have only // been permitted to edit the basic instance fields (name, etc.), so care must be taken not to display the config fields to // these users. Users who can add/edit course tools (mod/lti:addcoursetool) are able to view tool information anyway, via // the tool definitions, so this capability is used as a replacement, to control access to these tool config fields. $canviewmanualconfig = has_capability('mod/lti:addcoursetool', $this->context); $showtypes = has_capability('mod/lti:addpreconfiguredinstance', $this->context); if ($manualinstance && !$canviewmanualconfig) { // If you cannot add a manual instance and this is already a manual instance, then remove the 'types' selector. $showtypes = false; } $mform =& $this->_form; // Show the deprecation notice, regardless of whether the user can view the tool configuration details or not. // They will still see locked privacy fields and should be informed as to why that is. $mform->addElement('html', $OUTPUT->notification( get_string('editmanualinstancedeprecationwarning', 'mod_lti', get_docs_url('External_tool')), \core\output\notification::NOTIFY_WARNING, false) ); // Adding the "general" fieldset, where all the common settings are shown. $mform->addElement('html', ""); $mform->addElement('header', 'general', get_string('general', 'form')); // Adding the standard "name" field. $mform->addElement('text', 'name', get_string('basicltiname', 'lti'), ['size' => '64']); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); // Adding the optional "intro" and "introformat" pair of fields. $this->standard_intro_elements(get_string('basicltiintro', 'lti')); $mform->setAdvanced('introeditor'); // Display the label to the right of the checkbox so it looks better & matches rest of the form. if ($mform->elementExists('showdescription')) { $coursedesc = $mform->getElement('showdescription'); if (!empty($coursedesc)) { $coursedesc->setText(' ' . $coursedesc->getLabel()); $coursedesc->setLabel(' '); } } $mform->setAdvanced('showdescription'); $mform->addElement('checkbox', 'showtitlelaunch', get_string('display_name', 'lti')); $mform->setAdvanced('showtitlelaunch'); $mform->addHelpButton('showtitlelaunch', 'display_name', 'lti'); $mform->addElement('checkbox', 'showdescriptionlaunch', get_string('display_description', 'lti')); $mform->setAdvanced('showdescriptionlaunch'); $mform->addHelpButton('showdescriptionlaunch', 'display_description', 'lti'); if ($showtypes) { if ($manualinstance) { // Legacy, manually configured instances: only freeze the element (not hardFreeze) so that disabledIf() still works. // The data in the select is restricted so that only the current value is deemed valid, preventing DOM-edit changes, // which are possible with frozen elements. $tooltypes = $mform->addElement('select', 'typeid', get_string('external_tool_type', 'lti')); $mform->addHelpButton('typeid', 'external_tool_type', 'lti'); $manualinstanceoption = $instancetypes[0]; // The 'Automatic, based on tool URL' option. $tooltypes->addOption($manualinstanceoption->name, 0, []); $mform->freeze('typeid'); } else if ($matchestoolnotavailabletocourse) { // Legacy instances domain-matched to site tools: use a hidden field for typeid and a static visual element when // displaying these instances so that the string value of typeid is still visible when the element is frozen. // This gets around the fact that a frozen select without a selected option will display nothing. $mform->addElement('hidden', 'typeid', $typeid); $mform->setType('typeid', PARAM_INT); $manualinstanceoption = $instancetypes[0]; // The 'Automatic, based on tool URL' option. $mform->addElement('static', 'typeiddisplayonly', get_string('external_tool_type', 'lti'), $manualinstanceoption->name); } } else { // Need to submit these still, but hidden to avoid instructor modification. $mform->addElement('hidden', 'typeid', $typeid); $mform->setType('typeid', PARAM_INT); } // Disable the content selection button unconditionally. Freeze/hardFreeze is unsuitable for buttons. $mform->addElement('button', 'selectcontent', get_string('selectcontent', 'lti'), ['disabled' => 'disabled']); $mform->disabledIf('selectcontent', 'typeid', 'eq', $typeid); if ($canviewmanualconfig) { $mform->addElement('text', 'toolurl', get_string('launch_url', 'lti'), ['size' => '64']); $mform->setType('toolurl', PARAM_URL); $mform->addHelpButton('toolurl', 'launch_url', 'lti'); $mform->addElement('text', 'securetoolurl', get_string('secure_launch_url', 'lti'), ['size' => '64']); $mform->setType('securetoolurl', PARAM_URL); $mform->setAdvanced('securetoolurl'); $mform->addHelpButton('securetoolurl', 'secure_launch_url', 'lti'); } else { // Need to submit these still, but hidden to avoid instructor modification. $mform->addElement('hidden', 'toolurl', '', ['id' => 'id_toolurl']); $mform->setType('toolurl', PARAM_URL); $mform->addElement('hidden', 'securetoolurl', '', ['id' => 'id_securetoolurl']); $mform->setType('securetoolurl', PARAM_URL); } $mform->addElement('hidden', 'lineitemresourceid', '', ['id' => 'id_lineitemresourceid']); $mform->setType('lineitemresourceid', PARAM_TEXT); $mform->addElement('hidden', 'lineitemtag', '', ['id' => 'id_lineitemtag']); $mform->setType('lineitemtag', PARAM_TEXT); $mform->addElement('hidden', 'lineitemsubreviewurl', '', ['id' => 'id_lineitemsubreviewurl']); $mform->setType('lineitemsubreviewurl', PARAM_URL); $mform->addElement('hidden', 'lineitemsubreviewparams', '', ['id' => 'id_lineitemsubreviewparams']); $mform->setType('lineitemsubreviewparams', PARAM_TEXT); $launchoptions = [ LTI_LAUNCH_CONTAINER_DEFAULT => get_string('default', 'lti'), LTI_LAUNCH_CONTAINER_EMBED => get_string('embed', 'lti'), LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS => get_string('embed_no_blocks', 'lti'), LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW => get_string('existing_window', 'lti'), LTI_LAUNCH_CONTAINER_WINDOW => get_string('new_window', 'lti') ]; $mform->addElement('select', 'launchcontainer', get_string('launchinpopup', 'lti'), $launchoptions); $mform->addHelpButton('launchcontainer', 'launchinpopup', 'lti'); $mform->setAdvanced('launchcontainer'); if ($canviewmanualconfig) { $mform->addElement('text', 'resourcekey', get_string('resourcekey', 'lti')); $mform->setType('resourcekey', PARAM_TEXT); $mform->setAdvanced('resourcekey'); $mform->addHelpButton('resourcekey', 'resourcekey', 'lti'); $mform->setForceLtr('resourcekey'); $mform->addElement('passwordunmask', 'password', get_string('password', 'lti')); $mform->setType('password', PARAM_TEXT); $mform->setAdvanced('password'); $mform->addHelpButton('password', 'password', 'lti'); $mform->addElement('textarea', 'instructorcustomparameters', get_string('custom', 'lti'), ['rows' => 4, 'cols' => 60]); $mform->setType('instructorcustomparameters', PARAM_TEXT); $mform->setAdvanced('instructorcustomparameters'); $mform->addHelpButton('instructorcustomparameters', 'custom', 'lti'); $mform->setForceLtr('instructorcustomparameters'); $mform->addElement('text', 'icon', get_string('icon_url', 'lti'), ['size' => '64']); $mform->setType('icon', PARAM_URL); $mform->setAdvanced('icon'); $mform->addHelpButton('icon', 'icon_url', 'lti'); $mform->addElement('text', 'secureicon', get_string('secure_icon_url', 'lti'), ['size' => '64']); $mform->setType('secureicon', PARAM_URL); $mform->setAdvanced('secureicon'); $mform->addHelpButton('secureicon', 'secure_icon_url', 'lti'); } else { // Need to submit these still, but hidden to avoid instructor modification. $mform->addElement('hidden', 'resourcekey', '', ['id' => 'id_resourcekey']); $mform->setType('resourcekey', PARAM_TEXT); $mform->addElement('hidden', 'password', '', ['id' => 'id_password']); $mform->setType('password', PARAM_TEXT); $mform->addElement('hidden', 'instructorcustomparameters', '', ['id' => 'id_instructorcustomparameters']); $mform->setType('instructorcustomparameters', PARAM_TEXT); $mform->addElement('hidden', 'icon', '', ['id' => 'id_icon']); $mform->setType('icon', PARAM_URL); $mform->addElement('hidden', 'secureicon', '', ['id' => 'id_secureicon']); $mform->setType('secureicon', PARAM_URL); } // Add privacy preferences fieldset where users choose whether to send their data. $mform->addElement('header', 'privacy', get_string('privacy', 'lti')); $mform->addElement('advcheckbox', 'instructorchoicesendname', get_string('share_name', 'lti')); $mform->addHelpButton('instructorchoicesendname', 'share_name', 'lti'); $mform->addElement('advcheckbox', 'instructorchoicesendemailaddr', get_string('share_email', 'lti')); $mform->addHelpButton('instructorchoicesendemailaddr', 'share_email', 'lti'); $mform->addElement('advcheckbox', 'instructorchoiceacceptgrades', get_string('accept_grades', 'lti')); $mform->addHelpButton('instructorchoiceacceptgrades', 'accept_grades', 'lti'); // Add standard course module grading elements. $this->standard_grading_coursemodule_elements(); // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); $mform->setAdvanced('cmidnumber'); // Add standard buttons, common to all modules. $this->add_action_buttons(); $mform->hardFreeze([ 'toolurl', 'securetoolurl', 'launchcontainer', 'instructorcustomparameters', 'icon', 'secureicon', 'instructorchoicesendname', 'instructorchoicesendemailaddr', 'instructorchoiceacceptgrades' ]); } public function definition() { global $CFG, $PAGE, $OUTPUT, $COURSE; if ($this->type) { component_callback("ltisource_$this->type", 'add_instance_hook'); } // Determine whether this tool instance is a manually configure instance (now deprecated). $manualinstance = empty($this->current->typeid) && empty($this->typeid); // Determine whether this tool instance is using a domain-matched site tool which is not visible at the course level. // In such a case, the instance has a typeid (the site tool) and toolurl (the url used to domain match the site tool) set, // and the type still exists (is not deleted). $instancetypes = lti_get_types_for_add_instance(); $matchestoolnotavailabletocourse = false; if (!$manualinstance && !empty($this->current->toolurl)) { if (lti_get_type_config($this->current->typeid)) { $matchestoolnotavailabletocourse = !in_array($this->current->typeid, array_keys($instancetypes)); } } // Display the legacy form, presenting a read-only view of the configuration for unsupported (since 4.3) instances, which: // - Are manually configured instances (no longer supported. course tools should be configured and used instead). // - Are domain-matched to a hidden site level tool (no longer supported. to be replaced by URL-based course tool creation) // Instances based on preconfigured tools and which are not domain matched as above, are still valid and will be shown using // the non-legacy form. if ($manualinstance || $matchestoolnotavailabletocourse) { $this->legacy_instance_form_definition($instancetypes); return; } $tooltypeid = $this->current->typeid ?? $this->typeid; $tooltype = lti_get_type($tooltypeid); // Store the id of the tool type should it be linked to a tool proxy, to aid in disabling certain form elements. $toolproxytypeid = $tooltype->toolproxyid ? $tooltypeid : ''; $issitetooltype = $tooltype->course == get_site()->id; $mform =& $this->_form; // Adding the "general" fieldset, where all the common settings are shown. $mform->addElement('html', ""); $mform->addElement('header', 'general', get_string('general', 'form')); // For tools supporting content selection, add the 'Select content button'. $config = lti_get_type_config($tooltypeid); $supportscontentitemselection = !empty($config['contentitem']); if ($supportscontentitemselection) { $contentitemurl = new moodle_url('/mod/lti/contentitem.php'); $contentbuttonattributes = [ 'data-contentitemurl' => $contentitemurl->out(false), ]; // If this is an instance, was it created based on content selection in a prior-edit (need to infer since not stored). $iscontentitem = !empty($this->current->id) && (!empty($this->current->toolurl) || !empty($this->current->instructorcustomparameters) || !empty($this->current->secureicon) || !empty($this->current->icon)); $selectcontentindicatorinner = $iscontentitem ? $OUTPUT->pix_icon('i/valid', get_string('contentselected', 'mod_lti'), 'moodle', ['class' => 'me-1']) . get_string('contentselected', 'mod_lti') : ''; $selectcontentindicator = html_writer::div($selectcontentindicatorinner, '', ['aria-role' => 'status', 'id' => 'id_selectcontentindicator']); $selectcontentgrp = [ $mform->createElement('button', 'selectcontent', get_string('selectcontent', 'mod_lti'), $contentbuttonattributes, ['customclassoverride' => 'btn-secondary']), $mform->createElement('html', $selectcontentindicator), ]; $mform->addGroup($selectcontentgrp, 'selectcontentgroup', get_string('content'), ' ', false); } // Adding the standard "name" field. $mform->addElement('text', 'name', get_string('basicltiname', 'lti'), ['size' => '64']); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'server'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'server'); // Show activity name when launched only applies to embedded type launches. if (in_array($config['launchcontainer'], [LTI_LAUNCH_CONTAINER_EMBED, LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS])) { $mform->addElement('checkbox', 'showtitlelaunch', get_string('display_name', 'lti')); $mform->setDefault('showtitlelaunch', true); $mform->addHelpButton('showtitlelaunch', 'display_name', 'lti'); } else { // Include in the form anyway, so we retain the setting value in case the tool launch container is changed back. $mform->addElement('hidden', 'showtitlelaunch'); $mform->setType('showtitlelaunch', PARAM_BOOL); } // Adding the optional "intro" and "introformat" pair of fields. $this->standard_intro_elements(get_string('basicltiintro', 'lti')); // Display the label to the right of the checkbox so it looks better & matches rest of the form. if ($mform->elementExists('showdescription')) { $coursedesc = $mform->getElement('showdescription'); if (!empty($coursedesc)) { $coursedesc->setText(' ' . $coursedesc->getLabel()); $coursedesc->setLabel(' '); } } // Show activity description when launched only applies to embedded type launches. if (in_array($config['launchcontainer'], [LTI_LAUNCH_CONTAINER_EMBED, LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS])) { $mform->addElement('checkbox', 'showdescriptionlaunch', get_string('display_description', 'lti')); $mform->addHelpButton('showdescriptionlaunch', 'display_description', 'lti'); } else { // Include in the form anyway, so we retain the setting value in case the tool launch container is changed back. $mform->addElement('hidden', 'showdescriptionlaunch'); $mform->setType('showdescriptionlaunch', PARAM_BOOL); } // Tool settings. $mform->addElement('hidden', 'typeid', $tooltypeid, ['id' => 'hidden_typeid']); $mform->setType('typeid', PARAM_INT); if (!empty($config['contentitem'])) { $mform->addElement('hidden', 'contentitem', 1); $mform->setType('contentitem', PARAM_INT); } // Included to support deep linking return, but hidden to avoid instructor modification. $mform->addElement('hidden', 'toolurl', '', ['id' => 'id_toolurl']); $mform->setType('toolurl', PARAM_URL); $mform->addElement('hidden', 'securetoolurl', '', ['id' => 'id_securetoolurl']); $mform->setType('securetoolurl', PARAM_URL); $mform->addElement('hidden', 'urlmatchedtypeid', '', ['id' => 'id_urlmatchedtypeid']); $mform->setType('urlmatchedtypeid', PARAM_INT); $mform->addElement('hidden', 'lineitemresourceid', '', ['id' => 'id_lineitemresourceid']); $mform->setType('lineitemresourceid', PARAM_TEXT); $mform->addElement('hidden', 'lineitemtag', '', ['id' => 'id_lineitemtag']); $mform->setType('lineitemtag', PARAM_TEXT); $mform->addElement('hidden', 'lineitemsubreviewurl', '', ['id' => 'id_lineitemsubreviewurl']); $mform->setType('lineitemsubreviewurl', PARAM_URL); $mform->addElement('hidden', 'lineitemsubreviewparams', '', ['id' => 'id_lineitemsubreviewparams']); $mform->setType('lineitemsubreviewparams', PARAM_TEXT); // Launch container is set to 'LTI_LAUNCH_CONTAINER_DEFAULT', meaning it'll delegate to the tool's configuration. // Existing instances using values other than this can continue to use their existing value but cannot change it. $mform->addElement('hidden', 'launchcontainer', LTI_LAUNCH_CONTAINER_DEFAULT); $mform->setType('launchcontainer', PARAM_INT); // Included to support deep linking return, but hidden to avoid instructor modification. $mform->addElement('hidden', 'resourcekey', '', ['id' => 'id_resourcekey']); $mform->setType('resourcekey', PARAM_TEXT); $mform->addElement('hidden', 'password', '', ['id' => 'id_password']); $mform->setType('password', PARAM_TEXT); $mform->addElement('textarea', 'instructorcustomparameters', get_string('custom', 'lti'), ['rows' => 4, 'cols' => 60]); $mform->setType('instructorcustomparameters', PARAM_TEXT); $mform->setAdvanced('instructorcustomparameters'); $mform->addHelpButton('instructorcustomparameters', 'custom', 'lti'); $mform->setForceLtr('instructorcustomparameters'); $mform->addElement('hidden', 'icon', '', ['id' => 'id_icon']); $mform->setType('icon', PARAM_URL); $mform->addElement('hidden', 'secureicon', '', ['id' => 'id_secureicon']); $mform->setType('secureicon', PARAM_URL); // Add standard course module grading elements, and show them if the tool type + instance config permits it. if (!empty($config['acceptgrades']) && in_array($config['acceptgrades'], [LTI_SETTING_ALWAYS, LTI_SETTING_DELEGATE])) { $elementnamesbeforegrading = $this->_form->_elementIndex; $this->standard_grading_coursemodule_elements(); $elementnamesaftergrading = $this->_form->_elementIndex; // For all 'real' elements (not hidden or header) added as part of the standard grading elements, add a hideIf rule // making the element dependent on the 'accept grades from the tool' checkbox (instructorchoiceacceptgrades). $diff = array_diff($elementnamesaftergrading, $elementnamesbeforegrading); $diff = array_filter($diff, fn($key) => !in_array($this->_form->_elements[$key]->_type, ['hidden', 'header'])); foreach ($diff as $gradeelementname => $gradeelementindex) { $mform->hideIf($gradeelementname, 'instructorchoiceacceptgrades', 'eq', 0); } // Extend the grade section with the 'accept grades from the tool' checkbox, allowing per-instance overrides of that // value according to the following rules: // - Site tools with 'acceptgrades' set to 'ALWAYS' do not permit overrides at the instance level; the checkbox is // omitted in such cases. // - Site tools with 'acceptgrades' set to 'DELEGATE' result in a checkbox that is defaulted to unchecked but which // permits overrides to 'yes/checked'. // - Course tools with 'acceptgrades' set to 'ALWAYS' result in a checkbox that is defaulted to checked but which // permits overrides to 'no/unchecked'. // - Course tools with 'acceptgrades' set to 'DELEGATE' result in a checkbox that is defaulted to unchecked but which // permits overrides to 'yes/checked'. if (($issitetooltype && $config['acceptgrades'] == LTI_SETTING_DELEGATE) || !$issitetooltype) { $mform->insertElementBefore( $mform->createElement( 'advcheckbox', 'instructorchoiceacceptgrades', get_string('accept_grades_from_tool', 'mod_lti', $tooltype->name) ), array_keys($diff)[0] ); $acceptgradesdefault = !$issitetooltype && $config['acceptgrades'] == LTI_SETTING_ALWAYS ? '1' : '0'; $mform->setDefault('instructorchoiceacceptgrades', $acceptgradesdefault); $mform->disabledIf('instructorchoiceacceptgrades', 'typeid', 'in', [$toolproxytypeid]); // LTI 2 only. } } // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); $mform->setAdvanced('cmidnumber'); // Add standard buttons, common to all modules. $this->add_action_buttons(); if ($supportscontentitemselection) { $jsparams = [$COURSE->id]; // If local-network-access is enabled, pass the tool URL as well. $jsparams[] = (isset($CFG->ltiallowlocalnetwork) && $CFG->ltiallowlocalnetwork) ? $tooltype->baseurl : ''; $PAGE->requires->js_call_amd('mod_lti/mod_form', 'init', $jsparams); } } /** * Sets the current values handled by services in case of update. * * @param object $defaultvalues default values to populate the form with. */ public function set_data($defaultvalues) { $services = lti_get_services(); if (is_object($defaultvalues)) { foreach ($services as $service) { $service->set_instance_form_values( $defaultvalues ); } } parent::set_data($defaultvalues); } } toolproxies.php000064400000014577152265520610007666 0ustar00. /** * This file contains all necessary code to launch a Tool Proxy registration * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); // No guest autologin. require_login(0, false); $pageurl = new moodle_url('/mod/lti/toolproxies.php'); $PAGE->set_url($pageurl); admin_externalpage_setup('ltitoolproxies'); $PAGE->set_title(get_string('toolregistration', 'lti')); $configuredtoolproxieshtml = ''; $pendingtoolproxieshtml = ''; $acceptedtoolproxieshtml = ''; $rejectedtoolproxieshtml = ''; $configured = get_string('configured', 'lti'); $pending = get_string('pending', 'lti'); $accepted = get_string('accepted', 'lti'); $rejected = get_string('rejected', 'lti'); $name = get_string('name', 'lti'); $url = get_string('registrationurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); $toolproxies = $DB->get_records('lti_tool_proxies'); $configuredtoolproxies = lti_filter_tool_proxy_types($toolproxies, LTI_TOOL_PROXY_STATE_CONFIGURED); $configuredtoolproxieshtml = lti_get_tool_proxy_table($configuredtoolproxies, 'tp_configured'); $pendingtoolproxies = lti_filter_tool_proxy_types($toolproxies, LTI_TOOL_PROXY_STATE_PENDING); $pendingtoolproxieshtml = lti_get_tool_proxy_table($pendingtoolproxies, 'tp_pending'); $acceptedtoolproxies = lti_filter_tool_proxy_types($toolproxies, LTI_TOOL_PROXY_STATE_ACCEPTED); $acceptedtoolproxieshtml = lti_get_tool_proxy_table($acceptedtoolproxies, 'tp_accepted'); $rejectedtoolproxies = lti_filter_tool_proxy_types($toolproxies, LTI_TOOL_PROXY_STATE_REJECTED); $rejectedtoolproxieshtml = lti_get_tool_proxy_table($rejectedtoolproxies, 'tp_rejected'); $tab = optional_param('tab', '', PARAM_ALPHAEXT); $configuredselected = ''; $pendingselected = ''; $acceptedselected = ''; $rejectedselected = ''; switch ($tab) { case 'tp_pending': $pendingselected = 'class="selected"'; break; case 'tp_accepted': $acceptedselected = 'class="selected"'; break; case 'tp_rejected': $rejectedselected = 'class="selected"'; break; default: $configuredselected = 'class="selected"'; break; } $registertype = get_string('registertype', 'lti'); $config = get_string('manage_tools', 'lti'); $registertypeurl = "{$CFG->wwwroot}/mod/lti/registersettings.php?action=add&sesskey={$USER->sesskey}&tab=tool_proxy"; $template = <<< EOD
$configuredtoolproxieshtml
$pendingtoolproxieshtml
$acceptedtoolproxieshtml
$rejectedtoolproxieshtml
EOD; echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('manage_tool_proxies', 'lti'), 2); echo $OUTPUT->heading(new lang_string('toolproxy', 'lti') . $OUTPUT->help_icon('toolproxy', 'lti'), 3); echo $OUTPUT->box_start('generalbox'); echo $template; echo $OUTPUT->box_end(); echo $OUTPUT->footer(); thirdpartylibs.xml000064400000000575152266447110010351 0ustar00 ims-blti IMS-BLTI Set of IMS-LTI tools originaly provided by IMS. 1.0 MIT https://github.com/jfederico/ims-dev manage_deployment.php000064400000012554152266447110010764 0ustar00. /** * LTI 1.3 page to create or delete deployments. * * This page is only used by LTI 1.3. Older versions do not require platforms to be registered with the tool during * registration. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\output\notification; use enrol_lti\local\ltiadvantage\form\deployment_form; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use enrol_lti\local\ltiadvantage\service\tool_deployment_service; require_once(__DIR__ . '/../../config.php'); global $CFG, $OUTPUT; require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->dirroot . '/enrol/lti/lib.php'); $action = required_param('action', PARAM_ALPHA); $registrationid = required_param('registrationid', PARAM_INT); if (!in_array($action, ['add', 'delete'])) { throw new coding_exception("Invalid action param '$action'"); } // The page to go back to when the respective action has been performed. $deploymentslisturl = new moodle_url($CFG->wwwroot . "/enrol/lti/register_platform.php", ['regid' => $registrationid, 'action' => 'view', 'tabselect' => 'tooldeployments']); // Local anon helper to extend the nav for this page and call admin_externalpage_setup. $pagesetup = function(string $pagetitle) { global $PAGE; navigation_node::override_active_url( new moodle_url('/admin/settings.php', ['section' => 'enrolsettingslti_registrations']) ); admin_externalpage_setup('enrolsettingslti_deployment_manage', '', null, '', ['pagelayout' => 'admin']); $PAGE->navbar->add($pagetitle); }; // Local anon helper to map the formdata to the dto required for the domain layer. $maptodto = function($formdata): stdClass { return (object) [ 'registration_id' => $formdata->registrationid, 'deployment_name' => $formdata->name, 'deployment_id' => $formdata->deploymentid, ]; }; if ($action === 'add') { $pagesetup(get_string('deploymentadd', 'enrol_lti')); $pageurl = new moodle_url('/enrol/lti/manage_deployment.php', ['action' => 'add']); $mform = new deployment_form($pageurl->out(false)); if ($data = $mform->get_data()) { $deploymentservice = new tool_deployment_service(new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository()); $deploymentservice->add_tool_deployment($maptodto($data)); redirect($deploymentslisturl, get_string('deploymentaddnotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS); } else if (!$mform->is_cancelled()) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('deploymentadd', 'enrol_lti')); $mform->set_data([ 'registrationid' => $registrationid ]); $mform->display(); echo $OUTPUT->footer(); die(); } redirect($deploymentslisturl); } else if ($action === 'delete') { $id = required_param('id', PARAM_INT); $pagesetup(get_string('deploymentdelete', 'enrol_lti')); if (!optional_param('confirm', false, PARAM_BOOL)) { $continueparams = [ 'action' => 'delete', 'id' => $id, 'registrationid' => $registrationid, 'sesskey' => sesskey(), 'confirm' => true ]; $continueurl = new moodle_url('/enrol/lti/manage_deployment.php', $continueparams); $deploymentrepo = new deployment_repository(); $deployment = $deploymentrepo->find($id); if (!$deployment) { throw new coding_exception("Cannot delete non existent deployment '{$id}'."); } echo $OUTPUT->header(); echo $OUTPUT->confirm( get_string('deploymentdeleteconfirm', 'enrol_lti', format_string($deployment->get_deploymentid())), $continueurl, $deploymentslisturl ); echo $OUTPUT->footer(); } else { require_sesskey(); $deploymentservice = new tool_deployment_service(new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository()); $deploymentservice->delete_tool_deployment($id); redirect($deploymentslisturl, get_string('deploymentdeletenotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS); } } lang/en/enrol_lti.php000064400000041511152266447110010601 0ustar00. /** * LTI enrolment plugin version information * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['addcontent'] = 'Add content'; $string['adddeployment:name'] = 'Deployment name'; $string['adddeployment:deploymentid'] = 'Deployment ID'; $string['adddeployment:deploymentid_help'] = 'Each deployment of the tool (this site) in a platform will be assigned a Deployment ID that is unique to the registration. The Deployment ID must be registered with the tool (this site) before launches from the respective tool deployment are allowed.'; $string['adddeployment:invaliddeploymentiderror'] = 'Invalid deployment ID. The deployment ID already exists for this app registration.'; $string['addtocourse'] = 'Add to course'; $string['addtogradebook'] = 'Add to gradebook'; $string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.'; $string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.'; $string['cookiesarerequired'] = 'Cookies are blocked by your browser'; $string['cookiesarerequiredinfo'] = 'This tool can\'t be launched because your browser seems to be blocking third-party cookies.

To use this tool, try changing your browser cookie settings or using a different browser.'; $string['copiedtoclipboard'] = '{$a} copied to clipboard'; $string['copytoclipboard'] = 'Copy to clipboard'; $string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.'; $string['customproperties'] = 'Custom properties'; $string['deeplinkingurl'] = 'Deep linking URL'; $string['deletedactivity'] = 'Deleted activity'; $string['deletedactivityalt'] = 'The activity shared by this instance has been deleted'; $string['deletedactivitydescription'] = 'The activity shared by this instance has been deleted. You can select another activity to share by editing the instance or, if no longer required, you can simply delete the instance. Deleting the instance will remove any associated user enrolments.'; $string['deploymentadd'] = 'Add a deployment'; $string['deploymentaddnotice'] = 'Deployment added'; $string['deploymentdelete'] = 'Delete deployment'; $string['deploymentdeleteconfirm'] = 'Warning: Deleting a deployment will result in a loss of access for any users following resource links tied to this tool deployment in the platform. Member and grade sync services will also be removed for these resources. Are you sure you want to delete deployment ID \'{$a}\'?'; $string['deploymentdeletenotice'] = 'Deployment deleted'; $string['deploymentid'] = 'Deployment ID'; $string['deployments'] = 'Deployments'; $string['deploymentsinfo'] = 'A deployment ID is generated by the platform when the tool is made available in a context. This may occur at the time of registration, or later, when the tool is made available to a specific course or category. The deployment ID must be entered here before launches from the tool deployment are permitted.'; $string['details'] = 'Details'; $string['editplatformdetails'] = 'Edit platform details'; $string['endpointltiversionnotice'] = 'The tool endpoints below are for manual LTI 1.3 setup only. For earlier versions (1.1/2.0), details for consumers can be found on the \'Published as LTI tools\' page, accessible via course navigation.'; $string['enrolenddate'] = 'End date'; $string['enrolenddate_help'] = 'If enabled, users can access until this date only.'; $string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date'; $string['enrolisdisabled'] = 'The \'Publish as LTI tool\' plugin is disabled.'; $string['enrolltiversionincorrect'] = 'The resource is not set up for use over legacy LTI (versions 1.1/2.0). Please contact the administrator of this tool.'; $string['enrolperiod'] = 'Enrolment duration'; $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.'; $string['enrolmentfinished'] = 'Enrolment finished.'; $string['enrolmentnotstarted'] = 'Enrolment has not started.'; $string['enrolstartdate'] = 'Start date'; $string['enrolstartdate_help'] = 'If enabled, users can access from this date onward only.'; $string['existingregistrationerror'] = 'A registration already exists for this issuer and client ID.'; $string['frameembeddingnotenabled'] = 'To access the tool, please follow the link below.'; $string['failedrequest'] = 'Failed request. Reason: {$a->reason}'; $string['gradesync'] = 'Grade synchronisation'; $string['gradesync_help'] = 'Whether grades from the tool are sent to the remote system (LTI consumer).'; $string['incorrecttoken'] = 'The token was incorrect. Please check the URL and try again, or contact the administrator of this tool.'; $string['invalidexpiredregistrationurl'] = 'Invalid or expired registration URL. Please check the URL and try again.'; $string['invalidrequest'] = 'Invalid request'; $string['invalidtoolconsumer'] = 'Invalid tool consumer.'; $string['jwksurl'] = 'JWKS URL'; $string['loginurl'] = 'Initiate login URL'; $string['ltiadvdynregerror:invalidopenidconfigjson'] = "There was a problem fetching the OpenID configuration from the platform. The result was not valid JSON. This may also be caused by blocked hosts configuration. Please make sure your site is configured to connect to the platform domain and try again using a new registration URL."; $string['ltiadvlauncherror:missingid'] = 'Invalid launch data. The custom claim field \'id\' is required to launch an activity or resource over LTI Advantage.'; $string['ltiadvlauncherror:invalidid'] = 'Invalid launch data. The resource \'{$a}\' is unavailable or does not exist.'; $string['ltiadvlauncherror:invalidregistration'] = 'Invalid launch data or tool configuration. A platform registration could not be found (issuer: {$a->platform}, client ID: {$a->clientid}).'; $string['ltiadvlauncherror:invaliddeployment'] = 'Invalid launch data or tool configuration. The deployment could not be found (deployment ID: {$a}).'; $string['ltilegacy'] = 'Legacy LTI (1.1/2.0)'; $string['ltilegacydeprecatednotice'] = 'Legacy LTI (1.1/2.0) tools are deprecated. Content should now be shared using LTI Advantage.'; $string['lti13'] = 'LTI Advantage'; $string['lti13launchdetails'] = 'Launch details'; $string['lti13launchdetails_help'] = 'The launch URL and custom properties are only required when manually configuring a resource link in the platform. Where possible, teachers should favour the content selection (deep linking) process for the creation of resource links.'; $string['ltiversion'] = 'LTI version'; $string['ltiversion_help'] = 'The version of LTI which will be used to access the published content. LTI Advantage allows content to be published to pre-registered platforms without the need to make changes on the platform whenever new content is published. The security contract is between the platform and the tool. Legacy versions (1.1 and 2.0), require that a new tool registration is created on the consumer for each piece of newly published content because each piece of published content has its own security contract with the consumer site and must be set up on a case by case basis.'; $string['managedeployments'] = 'Manage deployments'; $string['maxenrolled'] = 'Maximum enrolled users'; $string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.'; $string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.'; $string['membersync'] = 'User synchronisation'; $string['membersync_help'] = 'Whether a scheduled task synchronises enrolled users in the remote system with enrolments in this course, creating an account for each remote user as necessary, and enrolling or unenrolling them as required. If set to no, at the moment when a remote user accesses the tool, an account will be created for them and they will be automatically enrolled.'; $string['membersyncmode'] = 'User synchronisation mode'; $string['membersyncmode_help'] = 'Whether remote users should be enrolled and/or unenrolled from this course.'; $string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing users'; $string['membersyncmodeenrolnew'] = 'Enrol new users'; $string['membersyncmodeunenrolmissing'] = 'Unenrol missing users'; $string['moodle'] = 'Moodle'; $string['nodeployments'] = 'No tool deployments found'; $string['nopublishedcontent'] = 'No resources or activities are published yet'; $string['notoolsprovided'] = 'No tools provided'; $string['noregisteredplatforms'] = 'No registered platforms'; $string['launchdetails'] = 'Launch details'; $string['launchdetails_help'] = 'A cartridge URL (also called configuration URL) plus secret or a launch URL are required for configuring the tool.'; $string['launchurl'] = 'Launch URL'; $string['lti:config'] = 'Configure \'Publish as LTI tool\' instances'; $string['lti:unenrol'] = 'Unenrol users from the course'; $string['opensslconfiginvalid'] = 'LTI 1.3 requires a valid openssl.cnf to be configured and available to your web server. Please contact the site administrator to configure and enable openssl for this site.'; $string['opentool'] = 'Open tool'; $string['publishedcontent'] = 'Published content'; $string['publishedcontent_help'] = 'A published resource or activity can be used in registered platforms via the content selection (deep linking) flow. Additionally, a resource link can be created manually in the platform by using the launch URL and the custom properties provided.'; $string['platformdetails'] = 'Platform details'; $string['platformdetailsinfo'] = 'Once the tool has been set up in the platform, details from the platform must be recorded here to complete the registration.
  • For dynamic registrations, this information will have been set automatically and no further changes are required.
  • For manual registrations, this information must be manually copied from the platform.
'; $string['pluginname'] = 'Publish as LTI tool'; $string['pluginname_desc'] = 'The \'Publish as LTI tool\' plugin, together with the LTI authentication plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.'; $string['privacy:metadata:enrol_lti_users'] = 'The list of users enrolled via an LTI provider'; $string['privacy:metadata:enrol_lti_users:userid'] = 'The ID of the user'; $string['privacy:metadata:enrol_lti_users:lastgrade'] = 'The last grade the user was recorded of having'; $string['privacy:metadata:enrol_lti_users:lastaccess'] = 'The time when the user last accessed the course'; $string['privacy:metadata:enrol_lti_users:timecreated'] = 'The time when the user was enrolled'; $string['provisioningmodestudentlaunch'] = 'Student first launch provisioning mode'; $string['provisioningmodeteacherlaunch'] = 'Teacher first launch provisioning mode'; $string['provisioningmode'] = 'Provisioning mode'; $string['provisioningmode_help'] = 'This setting determines how accounts are handled on first launch. Several modes are supported:
  • New accounts only (automatic). Accounts will be automatically created for users launching from the platform. This is the default for student launches.
  • Existing and new accounts (prompt). The user will be given a choice of what to do. They can decide to link an existing account, or have a new account created for them. This is the most flexible option and the default for teacher launches.
  • Existing accounts only (prompt). The user will be asked to link an existing account and cannot access the tool resources without doing so.
'; $string['registerplatform:accesstokenurl'] = 'Access token URL'; $string['registerplatform:accesstokenurl_help'] = 'The URL to which access token requests will be sent by the tool. Will be provided by the platform.'; $string['registerplatform:authrequesturl'] = 'Authentication request URL'; $string['registerplatform:authrequesturl_help'] = 'The URL to which OpenID Connect authentication requests will be sent by the tool. Will be provided by the platform.'; $string['registerplatform:clientid'] = 'Client ID'; $string['registerplatform:clientid_help'] = 'A string used to identify the tool registration on the platform. Will be provided by the platform.'; $string['registerplatform:duplicateregistrationerror'] = 'Invalid client ID. This client ID is already registered for the platform ID provided.'; $string['registerplatform:jwksurl'] = 'Public keyset URL'; $string['registerplatform:jwksurl_help'] = 'The public keyset or JWKS URL, used to get the platform\'s public key. Will be provided by the platform.'; $string['registerplatform:name'] = 'Platform name'; $string['registerplatform:name_help'] = 'A short name describing the platform. This can be changed at any time.'; $string['registerplatform:platformid'] = 'Platform ID (issuer)'; $string['registerplatform:platformid_help'] = 'The URL identifying the third party learning platform. Will be provided by the platform.'; $string['registerplatform:invalidurlerror'] = 'Invalid URL. Have you included http:// or https://?'; $string['registeredplatforms'] = 'Registered platforms'; $string['registeredplatformsltiversionnotice'] = 'The platforms listed below are registered for LTI 1.3 communication. For earlier versions, consumer registration is not required.'; $string['registerplatformadd'] = 'Register a platform'; $string['registerplatformaddnotice'] = 'Platform registration added'; $string['registerplatformdelete'] = 'Delete platform registration'; $string['registerplatformdeleteconfirm'] = 'Are you sure you want to delete the registration for the platform \'{$a}\'? This will also remove all tool deployments stored against this registration.'; $string['registerplatformdeletenotice'] = 'Platform registration deleted'; $string['registerplatformedit'] = 'Edit registration'; $string['registerplatformeditnotice'] = 'Platform registration updated'; $string['registration'] = 'Published tool registration'; $string['registrationdeeplinklabel'] = 'Add content from {$a}'; $string['registrationresourcelinklabel'] = 'Launch content from {$a}'; $string['registrationdynamic'] = 'Dynamic registration'; $string['registrationmanual'] = 'Manual registration'; $string['registrationstatus'] = 'Status'; $string['registrationstatusactive'] = 'Active'; $string['registrationstatuspending'] = 'Pending'; $string['registrationurl'] = 'Registration URL'; $string['registrationurlinfomessage'] = 'If the platform supports dynamic registration, use the registration URL below. For further information on dynamic registration, see the documentation Publish as LTI tool.'; $string['registrationurl_help'] = 'If a registration URL (also called proxy URL) is used, then the tool is automatically configured.'; $string['remotesystem'] = 'Remote system'; $string['requirecompletion'] = 'Require course or activity completion prior to grade synchronisation'; $string['returnurlnotset'] = 'Return URL was not set.'; $string['roleinstructor'] = 'Role for teacher'; $string['roleinstructor_help'] = 'The role assigned in the tool to the remote teacher.'; $string['rolelearner'] = 'Role for student'; $string['rolelearner_help'] = 'The role assigned in the tool to the remote student.'; $string['secret'] = 'Secret'; $string['secret_help'] = 'A string of characters which is shared with the remote system (LTI consumer) to provide access to the tool.'; $string['sharedexternaltools'] = 'Published as LTI tools'; $string['successfulregistration'] = 'Successful registration'; $string['tasksyncgrades'] = 'Publish as LTI tool grade sync'; $string['tasksyncmembers'] = 'Publish as LTI tool users sync'; $string['tooldetails'] = 'Tool details'; $string['toolsprovided'] = 'Published tools'; $string['toolsprovided_help'] = 'A tool may be shared with another site by providing either launch details or a registration URL.'; $string['tooltobeprovided'] = 'Tool to be published'; $string['toolurl'] = 'Tool URL'; $string['userdefaultvalues'] = 'User default values'; $string['viewplatformdetails'] = 'View platform details'; $string['viewtoolendpoints'] = 'View tool endpoints'; $string['cartridgeurl'] = 'Cartridge URL'; backup/moodle2/restore_enrol_lti_plugin.class.php000064400000007651152266447110016320 0ustar00. /** * Defines the restore_enrol_lti_plugin class. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Define all the restore steps. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_enrol_lti_plugin extends restore_enrol_plugin { /** * @var array $tools Stores the IDs of the newly created tools. */ protected $tools = array(); /** * Declares the enrol LTI XML paths attached to the enrol element * * @return array of {@link restore_path_element} */ protected function define_enrol_plugin_structure() { $paths = array(); $paths[] = new restore_path_element('enrol_lti_tool', $this->connectionpoint->get_path() . '/tool'); $paths[] = new restore_path_element('enrol_lti_users', $this->connectionpoint->get_path() . '/tool/users/user'); return $paths; } /** * Processes LTI tools element data * * @param array|stdClass $data */ public function process_enrol_lti_tool($data) { global $DB; $data = (object) $data; // Store the old id. $oldid = $data->id; // Change the values before we insert it. $data->timecreated = time(); $data->timemodified = $data->timecreated; // Set the correct legacy ltiversion when restoring old tools. if (empty($data->ltiversion)) { $data->ltiversion = 'LTI-1p0/LTI-2p0'; } // Generate a new uuid for LTI Advantage restores. if ($data->ltiversion == 'LTI-1p3') { $data->uuid = \core\uuid::generate(); } // Now we can insert the new record. $data->id = $DB->insert_record('enrol_lti_tools', $data); // Add the array of tools we need to process later. $this->tools[$data->id] = $data; // Set up the mapping. $this->set_mapping('enrol_lti_tool', $oldid, $data->id); } /** * Processes LTI users element data * * @param array|stdClass $data The data to insert as a comment */ public function process_enrol_lti_users($data) { global $DB; $data = (object) $data; $data->userid = $this->get_mappingid('user', $data->userid); $data->toolid = $this->get_mappingid('enrol_lti_tool', $data->toolid); $data->timecreated = time(); $DB->insert_record('enrol_lti_users', $data); } /** * This function is executed after all the tasks in the plan have been finished. * This must be done here because the activities have not been restored yet. */ public function after_restore_enrol() { global $DB; // Need to go through and change the values. foreach ($this->tools as $tool) { $updatetool = new stdClass(); $updatetool->id = $tool->id; $updatetool->enrolid = $this->get_mappingid('enrol', $tool->enrolid); $updatetool->contextid = $this->get_mappingid('context', $tool->contextid); $DB->update_record('enrol_lti_tools', $updatetool); } } } backup/moodle2/backup_enrol_lti_plugin.class.php000064400000005137152266447110016077 0ustar00. /** * Defines the backup_enrol_lti_plugin class. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Define all the backup steps. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_enrol_lti_plugin extends backup_enrol_plugin { /** * Defines the other LTI enrolment structures to append. * * @return backup_plugin_element */ public function define_enrol_plugin_structure() { // Get the parent we will be adding these elements to. $plugin = $this->get_plugin_element(); // Define our elements. $tool = new backup_nested_element('tool', array('id'), array( 'enrolid', 'contextid', 'institution', 'lang', 'timezone', 'maxenrolled', 'maildisplay', 'city', 'country', 'gradesync', 'gradesynccompletion', 'membersync', 'membersyncmode', 'roleinstructor', 'rolelearner', 'secret', 'ltiversion', 'timecreated', 'timemodified')); $users = new backup_nested_element('users'); $user = new backup_nested_element('user', array('id'), array( 'userid', 'toolid', 'serviceurl', 'sourceid', 'consumerkey', 'consumersecret', 'membershipurl', 'membershipsid')); // Build elements hierarchy. $plugin->add_child($tool); $tool->add_child($users); $users->add_child($user); // Set sources to populate the data. $tool->set_source_table('enrol_lti_tools', array('enrolid' => backup::VAR_PARENTID)); // Users are only added only if users included. if ($this->task->get_setting_value('users')) { $user->set_source_table('enrol_lti_users', array('toolid' => backup::VAR_PARENTID)); } } } pix/enrolinstancewarning.svg000064400000001112152266447110012322 0ustar00 pix/managedeployments.svg000064400000004551152266447110011616 0ustar00 pix/platformdetails.svg000064400000001164152266447110011271 0ustar00 register_platform.php000064400000017733152266447110011030 0ustar00. /** * LTI 1.3 page to create or edit a platform registration. * * This page is only used by LTI 1.3. Older versions do not require platforms to be registered with the tool during * registration. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\output\notification; use enrol_lti\local\ltiadvantage\form\create_registration_form; use enrol_lti\local\ltiadvantage\form\platform_registration_form; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use enrol_lti\local\ltiadvantage\service\application_registration_service; require_once(__DIR__ . '/../../config.php'); global $CFG, $OUTPUT, $DB; require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->dirroot . '/enrol/lti/lib.php'); $action = required_param('action', PARAM_ALPHA); if (!in_array($action, ['add', 'view', 'edit', 'delete'])) { throw new coding_exception("Invalid action param '$action'"); } // The page to go back to when the respective action has been performed. $toolregistrationurl = new moodle_url($CFG->wwwroot . "/" . $CFG->admin . "/settings.php", ['section' => 'enrolsettingslti_registrations']); // Local anon helper to extend the nav for this page and call admin_externalpage_setup. $pagesetup = function(string $pagetitle) { global $PAGE; navigation_node::override_active_url( new moodle_url('/admin/settings.php', ['section' => 'enrolsettingslti_registrations']) ); admin_externalpage_setup('enrolsettingslti_registrations_edit', '', null, '', ['pagelayout' => 'admin']); $PAGE->navbar->add($pagetitle); }; if ($action == 'view') { $regid = required_param('regid', PARAM_INT); $tabselect = optional_param('tabselect', 'platformdetails', PARAM_ALPHA); global $PAGE; $pagesetup(get_string('registerplatformedit', 'enrol_lti')); $pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'view', 'regid' => $regid]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('registerplatformedit', 'enrol_lti')); $renderer = $PAGE->get_renderer('enrol_lti'); echo $renderer->render_registration_view($regid, $tabselect); echo $OUTPUT->footer(); die(); } else if ($action === 'add') { $pagesetup(get_string('registerplatformadd', 'enrol_lti')); $pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'add']); $mform = new create_registration_form($pageurl->out(false)); if ($data = $mform->get_data()) { // Create the incomplete registration. $regservice = new application_registration_service(new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository()); $draft = $regservice->create_draft_application_registration($data); // Redirect to the registration view, which will display endpoints and allow the user to complete the registration. redirect(new moodle_url('/enrol/lti/register_platform.php', ['action' => 'view', 'regid' => $draft->get_id(), 'tabselect' => 'tooldetails'])); } else if (!$mform->is_cancelled()) { // Display the first step of registration creation. echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('registerplatformadd', 'enrol_lti')); $mform->display(); echo $OUTPUT->footer(); die(); } redirect($toolregistrationurl); } else if ($action === 'edit') { $regid = required_param('regid', PARAM_INT); $pagesetup(get_string('registerplatformedit', 'enrol_lti')); $pageurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'edit', 'regid' => $regid]); $viewurl = new moodle_url('/enrol/lti/register_platform.php', ['action' => 'view', 'regid' => $regid]); $mform = new platform_registration_form($pageurl->out(false)); if (($data = $mform->get_data()) && confirm_sesskey()) { $regservice = new application_registration_service(new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository()); $regservice->update_application_registration($data); redirect($viewurl, get_string('registerplatformeditnotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS); } else if (!$mform->is_cancelled()) { // Anon helper to transform data. $maptoformdata = function(application_registration $registration): \stdClass { return (object) [ 'id' => $registration->get_id(), 'name' => $registration->get_name(), 'platformid' => $registration->get_platformid(), 'clientid' => $registration->get_clientid(), 'authenticationrequesturl' => $registration->get_authenticationrequesturl(), 'jwksurl' => $registration->get_jwksurl(), 'accesstokenurl' => $registration->get_accesstokenurl() ]; }; $appregistrationrepo = new application_registration_repository(); $registration = $appregistrationrepo->find($regid); if (!$registration) { throw new coding_exception("cannot edit non-existent registration '{$regid}'."); } $mform->set_data($maptoformdata($registration)); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('registerplatformedit', 'enrol_lti')); $mform->display(); echo $OUTPUT->footer(); die(); } redirect($viewurl); } else if ($action === 'delete') { $regid = required_param('regid', PARAM_INT); $pagesetup(get_string('registerplatformdelete', 'enrol_lti')); if (!optional_param('confirm', false, PARAM_BOOL)) { $continueparams = ['action' => 'delete', 'regid' => $regid, 'sesskey' => sesskey(), 'confirm' => true]; $continueurl = new moodle_url('/enrol/lti/register_platform.php', $continueparams); $appregrepo = new application_registration_repository(); $appreg = $appregrepo->find($regid); if (!$appreg) { throw new coding_exception("Cannot delete non existent application registration '{$regid}'."); } echo $OUTPUT->header(); echo $OUTPUT->confirm( get_string('registerplatformdeleteconfirm', 'enrol_lti', format_string($appreg->get_name())), $continueurl, $toolregistrationurl ); echo $OUTPUT->footer(); } else { require_sesskey(); $regservice = new application_registration_service(new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository()); $regservice->delete_application_registration($regid); redirect($toolregistrationurl, get_string('registerplatformdeletenotice', 'enrol_lti'), null, notification::NOTIFY_SUCCESS); } } xml/imslticc.xml000064400000002672152266447110007714 0ustar00 proxy.php000064400000005123152266447110006447 0ustar00. /** * Tool proxy. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); $toolid = null; $token = null; $filearguments = get_file_argument(); $arguments = explode('/', trim($filearguments, '/')); if (count($arguments) == 2) { list($toolid, $token) = $arguments; } $toolid = optional_param('id', $toolid, PARAM_INT); $token = optional_param('token', $token, PARAM_ALPHANUM); $PAGE->set_context(context_system::instance()); $url = new moodle_url('/enrol/lti/tp.php'); $PAGE->set_url($url); $PAGE->set_pagelayout('popup'); $PAGE->set_title(get_string('registration', 'enrol_lti')); // Only show the proxy if the token parameter is correct. // If we do not compare with a shared secret, someone could very easily // guess an id for the enrolment. if (!\enrol_lti\helper::verify_proxy_token($toolid, $token)) { throw new \moodle_exception('incorrecttoken', 'enrol_lti'); } $tool = \enrol_lti\helper::get_lti_tool($toolid); if (!is_enabled_auth('lti')) { throw new \moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti')); exit(); } // Check if the enrolment plugin is disabled. if (!enrol_is_enabled('lti')) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); exit(); } // Check if the enrolment instance is disabled. if ($tool->status != ENROL_INSTANCE_ENABLED) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); exit(); } $messagetype = required_param('lti_message_type', PARAM_TEXT); // Only accept proxy registration requests from this endpoint. if ($messagetype != "ToolProxyRegistrationRequest") { throw new \moodle_exception('invalidrequest', 'enrol_lti'); exit(); } $toolprovider = new \enrol_lti\tool_provider($toolid); $toolprovider->handleRequest(); echo $OUTPUT->header(); echo $OUTPUT->footer(); jwks.php000064400000002516152266447110006247 0ustar00. /** * This file returns an array of available public keys for the LTI 1.3 tool. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use Packback\Lti1p3\JwksEndpoint; define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . '/../../config.php'); $privatekey = get_config('enrol_lti', 'lti_13_privatekey'); $key = get_config('enrol_lti', 'lti_13_kid'); $keyendpoint = JwksEndpoint::new([$key => $privatekey]); @header('Content-Type: application/json; charset=utf-8'); echo json_encode($keyendpoint->getPublicJwks()); classes/manage_table.php000064400000023222152266447110011322 0ustar00. /** * Displays enrolment LTI instances. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir . '/tablelib.php'); /** * Handles displaying enrolment LTI instances. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manage_table extends \table_sql { /** * @var \enrol_plugin $ltiplugin */ protected $ltiplugin; /** * @var bool $ltienabled */ protected $ltienabled; /** * @var bool $canconfig */ protected $canconfig; /** * @var int $courseid The course id. */ protected $courseid; /** * Sets up the table. * * @param string $courseid The id of the course. */ public function __construct($courseid) { parent::__construct('enrol_lti_manage_table'); $this->define_columns(array( 'name', 'launch', 'registration', 'edit' )); $this->define_headers(array( get_string('name'), get_string('launchdetails', 'enrol_lti'), get_string('registrationurl', 'enrol_lti'), get_string('edit') )); $this->collapsible(false); $this->sortable(false); // Set the variables we need access to. $this->ltiplugin = enrol_get_plugin('lti'); $this->ltienabled = enrol_is_enabled('lti'); $this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid)); $this->courseid = $courseid; // Set help icons. $launchicon = new \help_icon('launchdetails', 'enrol_lti'); $regicon = new \help_icon('registrationurl', 'enrol_lti'); $this->define_help_for_headers(['1' => $launchicon, '2' => $regicon]); } /** * Generate the name column. * * @param \stdClass $tool event data. * @return string */ public function col_name($tool) { $toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING); $name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool); return $this->get_display_text($tool, $name); } /** * Generate the launch column. * * @param \stdClass $tool instance data. * @return string */ public function col_launch($tool) { global $OUTPUT; $url = helper::get_cartridge_url($tool); $cartridgeurllabel = get_string('cartridgeurl', 'enrol_lti'); $cartridgeurl = $url; $secretlabel = get_string('secret', 'enrol_lti'); $secret = $tool->secret; $launchurl = helper::get_launch_url($tool->id); $launchurllabel = get_string('launchurl', 'enrol_lti'); $data = [ "rows" => [ [ "label" => $cartridgeurllabel, "text" => $cartridgeurl, "id" => "cartridgeurl", "hidelabel" => false ], [ "label" => $secretlabel, "text" => $secret, "id" => "secret", "hidelabel" => false ], [ "label" => $launchurllabel, "text" => $launchurl, "id" => "launchurl", "hidelabel" => false ], ] ]; $return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data); return $return; } /** * Generate the Registration column. * * @param \stdClass $tool instance data. * @return string */ public function col_registration($tool) { global $OUTPUT; $url = helper::get_proxy_url($tool); $toolurllabel = get_string("registrationurl", "enrol_lti"); $toolurl = $url; $data = [ "rows" => [ [ "label" => $toolurllabel, "text" => $toolurl, "id" => "toolurl" , "hidelabel" => true], ] ]; $return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data); return $return; } /** * Generate the edit column. * * @param \stdClass $tool event data. * @return string */ public function col_edit($tool) { global $OUTPUT; $buttons = array(); $instance = new \stdClass(); $instance->id = $tool->enrolid; $instance->courseid = $tool->courseid; $instance->enrol = 'lti'; $instance->status = $tool->status; $strdelete = get_string('delete'); $strenable = get_string('enable'); $strdisable = get_string('disable'); $url = new \moodle_url('/enrol/lti/index.php', array('sesskey' => sesskey(), 'courseid' => $this->courseid, 'legacy' => 1)); if ($this->ltiplugin->can_delete_instance($instance)) { $aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core', array('class' => 'iconsmall'))); } if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) { if ($instance->status == ENROL_INSTANCE_ENABLED) { $aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core', array('class' => 'iconsmall'))); } else if ($instance->status == ENROL_INSTANCE_DISABLED) { $aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core', array('class' => 'iconsmall'))); } } if ($this->ltienabled && $this->canconfig) { $linkparams = array( 'courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol, 'legacy' => 1, 'returnurl' => new \moodle_url('/enrol/lti/index.php', array('courseid' => $this->courseid, 'legacy' => 1)) ); $editlink = new \moodle_url("/enrol/editinstance.php", $linkparams); $buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core', array('class' => 'iconsmall'))); } return implode(' ', $buttons); } /** * Query the reader. Store results in the object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. */ public function query_db($pagesize, $useinitialsbar = true) { $total = \enrol_lti\helper::count_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p0/LTI-2p0']); $this->pagesize($pagesize, $total); $tools = \enrol_lti\helper::get_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p0/LTI-2p0'], $this->get_page_start(), $this->get_page_size()); $this->rawdata = $tools; // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Returns text to display in the columns. * * @param \stdClass $tool the tool * @param string $text the text to alter * @return string */ protected function get_display_text($tool, $text) { if ($tool->status != ENROL_INSTANCE_ENABLED) { return \html_writer::tag('div', $text, array('class' => 'dimmed_text')); } return $text; } /** * Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted. * * @param \stdClass $tool the tool instance record. * @return string the HTML for the name column. */ protected function get_deleted_activity_name_html(\stdClass $tool): string { global $OUTPUT; $icon = \html_writer::tag( 'a', $OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [ "class" => "btn btn-link p-0", "role" => "button", "data-bs-container" => "body", "data-bs-toggle" => "popover", "data-bs-placement" => right_to_left() ? "left" : "right", "data-bs-content" => get_string('deletedactivitydescription', 'enrol_lti'), "data-bs-html" => "true", "tabindex" => "0", "data-bs-trigger" => "focus", ] ); $name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti')); if ($tool->name) { $name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name; } return $name; } } classes/local/ltiadvantage/form/deployment_form.php000064400000006240152266447110016627 0ustar00. namespace enrol_lti\local\ltiadvantage\form; use enrol_lti\local\ltiadvantage\repository\deployment_repository; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * The deployment_form class, for registering a deployment for a registered platform. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class deployment_form extends \moodleform { /** * Define the form. */ protected function definition() { $mform = $this->_form; $strrequired = get_string('required'); // Registration id. $mform->addElement('hidden', 'registrationid'); $mform->setType('registrationid', PARAM_INT); // Name. $mform->addElement('text', 'name', get_string('adddeployment:name', 'enrol_lti')); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', $strrequired, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); // Deployment Id. $mform->addElement('text', 'deploymentid', get_string('adddeployment:deploymentid', 'enrol_lti')); $mform->setType('deploymentid', PARAM_TEXT); $mform->addRule('deploymentid', $strrequired, 'required', null, 'client'); $mform->addRule('deploymentid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('deploymentid', 'adddeployment:deploymentid', 'enrol_lti'); $buttonarray = []; $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges')); $buttonarray[] = $mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', ' ', false); } /** * Provides uniqueness validation of the deployment id. * * @param array $data any form data * @param array $files any submitted files * @return array array of errors. */ public function validation($data, $files) { $errors = parent::validation($data, $files); // Validate the uniqueness of the deploymentid within the registration. $deploymentrepo = new deployment_repository(); if ($deploymentrepo->find_by_registration($data['registrationid'], $data['deploymentid'])) { $errors['deploymentid'] = get_string('adddeployment:invaliddeploymentiderror', 'enrol_lti'); } return $errors; } } classes/local/ltiadvantage/form/platform_registration_form.php000064400000011616152266447110021070 0ustar00. namespace enrol_lti\local\ltiadvantage\form; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * The platform_registration_form class, for registering a platform as a consumer of a published tool. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class platform_registration_form extends \moodleform { /** * Define the form. */ protected function definition() { $mform = $this->_form; $strrequired = get_string('required'); // Id. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); // Name. $mform->addElement('text', 'name', get_string('registerplatform:name', 'enrol_lti')); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', $strrequired, 'required', null, 'client'); // Platform Id. $mform->addElement('text', 'platformid', get_string('registerplatform:platformid', 'enrol_lti')); $mform->setType('platformid', PARAM_URL); $mform->addRule('platformid', $strrequired, 'required', null, 'client'); $mform->addHelpButton('platformid', 'registerplatform:platformid', 'enrol_lti'); // Client Id. $mform->addElement('text', 'clientid', get_string('registerplatform:clientid', 'enrol_lti')); $mform->setType('clientid', PARAM_TEXT); $mform->addRule('clientid', $strrequired, 'required', null, 'client'); $mform->addHelpButton('clientid', 'registerplatform:clientid', 'enrol_lti'); // Authentication request URL. $mform->addElement('text', 'authenticationrequesturl', get_string('registerplatform:authrequesturl', 'enrol_lti')); $mform->setType('authenticationrequesturl', PARAM_URL); $mform->addRule('authenticationrequesturl', $strrequired, 'required', null, 'client'); $mform->addHelpButton('authenticationrequesturl', 'registerplatform:authrequesturl', 'enrol_lti'); // JSON Web Key Set URL. $mform->addElement('text', 'jwksurl', get_string('registerplatform:jwksurl', 'enrol_lti')); $mform->setType('jwksurl', PARAM_URL); $mform->addRule('jwksurl', $strrequired, 'required', null, 'client'); $mform->addHelpButton('jwksurl', 'registerplatform:jwksurl', 'enrol_lti'); // Access token URL. $mform->addElement('text', 'accesstokenurl', get_string('registerplatform:accesstokenurl', 'enrol_lti')); $mform->setType('accesstokenurl', PARAM_URL); $mform->addRule('accesstokenurl', $strrequired, 'required', null, 'client'); $mform->addHelpButton('accesstokenurl', 'registerplatform:accesstokenurl', 'enrol_lti'); $buttonarray = []; $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges')); $buttonarray[] = $mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', ' ', false); } /** * Provides validation of URL syntax and issuer uniqueness. * * @param array $data the form data. * @param array $files any submitted files. * @return array an array of errors. */ public function validation($data, $files) { $errors = parent::validation($data, $files); // Check URLs look ok. foreach ($data as $key => $val) { if (isset($this->_form->_types[$key]) && $this->_form->_types[$key] == 'url') { if (!filter_var($val, FILTER_VALIDATE_URL)) { $errors[$key] = get_string('registerplatform:invalidurlerror', 'enrol_lti'); } } } // Check uniqueness of the {issuer, client_id} tuple. $appregistrationrepo = new application_registration_repository(); $appreg = $appregistrationrepo->find_by_platform($data['platformid'], $data['clientid']); if ($appreg) { if (empty($data['id']) || $appreg->get_id() != $data['id']) { $errors['clientid'] = get_string('registerplatform:duplicateregistrationerror', 'enrol_lti'); } } return $errors; } } classes/local/ltiadvantage/form/create_registration_form.php000064400000003206152266447110020503 0ustar00. namespace enrol_lti\local\ltiadvantage\form; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * The create_registration_form class, for creating a new pending platform registration. * * @package enrol_lti * @copyright 2022 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class create_registration_form extends \moodleform { /** * Define the form. */ protected function definition() { $mform = $this->_form; // Name. $mform->addElement('text', 'name', get_string('registerplatform:name', 'enrol_lti')); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->addHelpButton('name', 'registerplatform:name', 'enrol_lti'); // Continue/cancel buttons. $this->add_action_buttons(true, get_string('continue')); } } classes/local/ltiadvantage/lib/lti_cookie.php000064400000004546152266447110015357 0ustar00. namespace enrol_lti\local\ltiadvantage\lib; use core\session\utility\cookie_helper; use Packback\Lti1p3\Interfaces\ICookie; /** * Cookie representation used by the lti1p3 library code. * * This implementation is a copy of the Packback ImsCookie implementation, a class previously included in the library * but which is now deprecated there. * * @package enrol_lti * @copyright 2024 Jake Dallimore time() + $exp, ]; // SameSite none and secure will be required for tools to work inside iframes. $samesiteoptions = [ 'samesite' => 'None', 'secure' => true, ]; setcookie($name, $value, array_merge($cookieoptions, $samesiteoptions, $options)); // Necessary, since partitioned can't be set via setcookie yet. cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']); // Set a second fallback cookie in the event that "SameSite" is not supported. setcookie('LEGACY_'.$name, $value, array_merge($cookieoptions, $options)); } } classes/local/ltiadvantage/lib/launch_cache_session.php000064400000007252152266447110017373 0ustar00. namespace enrol_lti\local\ltiadvantage\lib; use Packback\Lti1p3\Interfaces\ICache; /** * The launch_cache_session, providing a temporary session store for launch information. * * This is used to store the launch information while the user is transitioned through the Moodle authentication flows * and back to the deep linking launch handler (launch_deeplink.php). * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class launch_cache_session implements ICache { /** * Get the launch data from the cache. * * @param string $key the launch id. * @return array|null the launch data. */ public function getLaunchData(string $key): ?array { global $SESSION; if (isset($SESSION->enrol_lti_launch[$key])) { return unserialize($SESSION->enrol_lti_launch[$key]); } return null; } /** * Add launch data to the cache. * * @param string $key the launch id. * @param array $jwtBody the launch data. */ public function cacheLaunchData(string $key, array $jwtBody): void { global $SESSION; $SESSION->enrol_lti_launch[$key] = serialize($jwtBody); } /** * Cache the nonce. * * @param string $nonce the nonce. * @param string $state the state. */ public function cacheNonce(string $nonce, string $state): void { global $SESSION; $SESSION->enrol_lti_launch_nonce[$nonce] = $state; } /** * Check whether the cache contains the nonce. * * @param string $nonce the nonce * @param string $state the state * @return bool true if found, false otherwise. */ public function checkNonceIsValid(string $nonce, string $state): bool { global $SESSION; return isset($SESSION->enrol_lti_launch_nonce[$nonce]) && $SESSION->enrol_lti_launch_nonce[$nonce] == $state; } /** * Delete all data from the session cache. */ public function purge() { global $SESSION; unset($SESSION->enrol_lti_launch); } /** * Cache the access token. * * @param string $key the key * @param string $accessToken the access token */ public function cacheAccessToken(string $key, string $accessToken): void { global $SESSION; $SESSION->enrol_lti_launch_token[$key] = $accessToken; } /** * Get a cached access token. * * @param string $key the key to check. * @return string|null the token string, or null if not found. */ public function getAccessToken(string $key): ?string { global $SESSION; return $SESSION->enrol_lti_launch_token[$key] ?? null; } /** * Clear the access token from the cache. * * @param string $key the key to purge. */ public function clearAccessToken(string $key): void { global $SESSION; unset($SESSION->enrol_lti_launch_token[$key]); } } classes/local/ltiadvantage/lib/issuer_database.php000064400000013022152266447110016361 0ustar00. namespace enrol_lti\local\ltiadvantage\lib; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use Packback\Lti1p3\Interfaces\IDatabase; use Packback\Lti1p3\LtiDeployment; use Packback\Lti1p3\LtiRegistration; /** * The issuer_database class, providing a read-only store of issuer details. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class issuer_database implements IDatabase { /** @var application_registration_repository an application registration repository instance used for lookups.*/ private $appregrepo; /** @var deployment_repository a deployment repository instance for lookups.*/ private $deploymentrepo; /** * The issuer_database constructor. * @param application_registration_repository $appregrepo an application registration repository instance. * @param deployment_repository $deploymentrepo a deployment repository instance. */ public function __construct(application_registration_repository $appregrepo, deployment_repository $deploymentrepo) { $this->appregrepo = $appregrepo; $this->deploymentrepo = $deploymentrepo; } /** * Find and return an LTI registration based on its unique {issuer, client_id} tuple. * * @param string $iss the issuer id. * @param string|null $clientId the client_id of the registration. * @return LtiRegistration|null The registration object, or null if not found. */ public function findRegistrationByIssuer(string $iss, ?string $clientId = null): ?LtiRegistration { if (is_null($clientId)) { throw new \coding_exception("The param 'clientid' is required. Calling code must either pass in 'client_id' ". "(generated by the platform during registration) or 'id' (found in the initiate login URI created by the tool) ". "to identify the client."); } global $CFG; require_once($CFG->libdir . '/moodlelib.php'); // For get_config() usage. // We can identify registrations two ways. Either: // 1. Using issuer + the platform-generated clientid. Most platforms will have sent client_id in the initiate login request // despite it being an optional param in the spec. They must include it as the aud value in the resource link request JWT. // 2. Using issuer + a tool-generated ID. This supports platforms which omit client_id during a login call. Using the ID // that is a part of the initiate login URI allows Moodle to locate the registration for that unique client, without the // platform-generated client_id. // Major platforms will likely include client_id in the login request, so favor that approach first, only falling back on // the local id approach where a registration cannot be found in the first instance. $reg = $this->appregrepo->find_by_platform($iss, $clientId); if (!$reg) { $reg = $this->appregrepo->find_by_platform_uniqueid($iss, $clientId); } if (!$reg) { return null; } $privatekey = get_config('enrol_lti', 'lti_13_privatekey'); $kid = get_config('enrol_lti', 'lti_13_kid'); return LtiRegistration::new() ->setAuthLoginUrl($reg->get_authenticationrequesturl()->out(false)) ->setAuthTokenUrl($reg->get_accesstokenurl()->out(false)) ->setClientId($reg->get_clientid()) ->setKeySetUrl($reg->get_jwksurl()->out(false)) ->setKid($kid) ->setIssuer($reg->get_platformid()->out(false)) ->setToolPrivateKey($privatekey); } /** * Returns an LTI deployment based on the {issuer, client_id} tuple and a deployment id string. * * @param string $iss the issuer id. * @param string $deploymentId the deployment id. * @param string|null $clientId the client_id of the registration. * @return LtiDeployment|null The deployment object or null if not found. */ public function findDeployment(string $iss, string $deploymentId, ?string $clientId = null): ?LtiDeployment { if (is_null($clientId)) { throw new \coding_exception("Both issuer and client id are required to identify platform registrations ". "and must be included in the 'aud' claim of the message JWT."); } $appregistration = $this->appregrepo->find_by_platform($iss, $clientId); if (!$appregistration) { return null; } $deployment = $this->deploymentrepo->find_by_registration($appregistration->get_id(), $deploymentId); if (!$deployment) { return null; } return LtiDeployment::new($deployment->get_deploymentid()); } } classes/local/ltiadvantage/viewobject/published_resource.php000064400000012222152266447110020505 0ustar00. namespace enrol_lti\local\ltiadvantage\viewobject; /** * The class published_resource, instances of which represent a specific VIEW of a published resource. * * This class performs no validation and is only meant to be used as a slice of the existing data for use in the * content selection flow. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class published_resource { /** @var string the name of this resource. */ private $name; /** @var string full name of the course to which this published resource belongs. */ private $coursefullname; /** @var int id of the course to which this published resource belongs. */ private $courseid; /** @var int the context id of the resource */ private $contextid; /** @var int id of the enrol_lti_tools instance (i.e. the id of the 'published resource'). */ private $id; /** @var string a v4 uuid identifier for this published resource. */ private $uuid; /** @var bool whether or not this resource supports grades. */ private $supportsgrades; /** @var float the max grade or null if not a graded resource. */ private $grademax; /** @var bool whether or not this resource is itself a course. */ private $iscourse; /** * The published_resource constructor. * * @param string $name the name of this resource. * @param string $coursefullname full name of the course to which this published resource belongs. * @param int $courseid id of the course to which this published resource belongs. * @param int $contextid id of the context. * @param int $id id of the enrol_lti_tools instance (i.e. the id of the 'published resource'). * @param string $uuid a v4 uuid identifier for this published resource. * @param bool $supportsgrades whether or not this resource supports grades. * @param float|null $grademax the max grade or null if this is not a graded resource. * @param bool $iscourse whether or not this resource is itself a course. */ public function __construct(string $name, string $coursefullname, int $courseid, int $contextid, int $id, string $uuid, bool $supportsgrades, ?float $grademax, bool $iscourse) { $this->name = $name; $this->coursefullname = $coursefullname; $this->courseid = $courseid; $this->contextid = $contextid; $this->id = $id; $this->uuid = $uuid; $this->supportsgrades = $supportsgrades; $this->grademax = $grademax; $this->iscourse = $iscourse; } /** * Get the name of this published resource. * * @return string the localised name. */ public function get_name(): string { return $this->name; } /** * Get the full name of the course owning this published resource. * * @return string the localised course full name. */ public function get_coursefullname(): string { return $this->coursefullname; } /** * Get the id of the course owning this published resource. * * @return int the course id. */ public function get_courseid(): int { return $this->courseid; } /** * Get the id of the context for this published resource. * * @return int the context id. */ public function get_contextid(): int { return $this->contextid; } /** * Get the id of this published resource. * * @return int the id. */ public function get_id(): int { return $this->id; } /** * Get the uuid for this published resource. * * @return string v4 uuid. */ public function get_uuid(): string { return $this->uuid; } /** * Check whether this resource supports grades or not. * * @return bool true if supported, false otherwise. */ public function supports_grades(): bool { return $this->supportsgrades; } /** * Get the max grade for this published resource, if its a graded resource. * * @return float|null the grade max, if grades are supported, else null. */ public function get_grademax(): ?float { return $this->grademax; } /** * Check whether this published resource is a course itself. * * @return bool true if it's a course, false otherwise. */ public function is_course(): bool { return $this->iscourse; } } classes/local/ltiadvantage/entity/ags_info.php000064400000015360152266447110015565 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * The ags_info class, instances of which represent grade service information for a resource_link or context. * * For information about Assignment and Grade Services 2.0, see https://www.imsglobal.org/spec/lti-ags/v2p0/. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ags_info { /** @var string Scope for lineitem management, used when a platform allows the tool to create lineitems.*/ private const SCOPES_LINEITEM_MANAGE = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; /** @var string Scope for lineitem reads, used when a tool only grants read access to line items.*/ private const SCOPES_LINEITEM_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; /** @var string Scope for reading results.*/ private const SCOPES_RESULT_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** @var string Scope for posting scores.*/ private const SCOPES_SCORES_POST = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** @var \moodle_url|null The service URL used to get/put lineitems, if supported*/ private $lineitemsurl; /** @var \moodle_url|null The lineitemurl, which is only present when a single lineitem is supported.*/ private $lineitemurl; /** @var array The array of supported lineitem-related scopes for this service instance.*/ private $lineitemscopes = []; /** @var string|null The supported result scope for this service instance.*/ private $resultscope = null; /** @var string|null The supported score scope for this service instance.*/ private $scorescope = null; /** * The ags_info constructor. * * @param \moodle_url|null $lineitemsurl The service URL used to get/put lineitems, if supported. * @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported. * @param array $scopes The array of supported scopes for this service instance. */ private function __construct(?\moodle_url $lineitemsurl, ?\moodle_url $lineitemurl, array $scopes) { // Platforms may support just lineitemurl, just lineitemsurl or both. At least one of the two is required. if (is_null($lineitemsurl) && is_null($lineitemurl)) { throw new \coding_exception("Missing lineitem or lineitems URL"); } $this->lineitemsurl = $lineitemsurl; $this->lineitemurl = $lineitemurl; $this->validate_scopes($scopes); } /** * Factory method to create a new ags_info instance. * * @param \moodle_url|null $lineitemsurl The service URL used to get/put lineitems, if supported. * @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported. * @param array $scopes The array of supported scopes for this service instance. * @return ags_info the object instance. */ public static function create(?\moodle_url $lineitemsurl = null, ?\moodle_url $lineitemurl = null, array $scopes = []): ags_info { return new self($lineitemsurl, $lineitemurl, $scopes); } /** * Check the supplied scopes for validity and set instance vars if appropriate. * * @param array $scopes the array of string scopes to check. * @throws \coding_exception if any of the scopes is invalid. */ private function validate_scopes(array $scopes): void { $supportedscopes = [ self::SCOPES_LINEITEM_READONLY, self::SCOPES_LINEITEM_MANAGE, self::SCOPES_RESULT_READONLY, self::SCOPES_SCORES_POST ]; foreach ($scopes as $scope) { if (!is_string($scope)) { throw new \coding_exception('Scope must be a string value'); } $key = array_search($scope, $supportedscopes); if ($key === 0) { $this->lineitemscopes[] = self::SCOPES_LINEITEM_READONLY; } else if ($key === 1) { $this->lineitemscopes[] = self::SCOPES_LINEITEM_MANAGE; } else if ($key === 2) { $this->resultscope = self::SCOPES_RESULT_READONLY; } else if ($key === 3) { $this->scorescope = self::SCOPES_SCORES_POST; } } } /** * Get the url for querying line items, if supported. * * @return \moodle_url the url. */ public function get_lineitemsurl(): ?\moodle_url { return $this->lineitemsurl; } /** * Get the single line item url, in cases where only one line item exists. * * @return \moodle_url|null the url, or null if not present. */ public function get_lineitemurl(): ?\moodle_url { return $this->lineitemurl; } /** * Get the authorization scope for lineitems. * * @return array|null the scopes, if present, else null. */ public function get_lineitemscope(): ?array { return !empty($this->lineitemscopes) ? $this->lineitemscopes : null; } /** * Get the authorization scope for results. * * @return string|null the scope, if present, else null. */ public function get_resultscope(): ?string { return $this->resultscope; } /** * Get the authorization scope for scores. * * @return string|null the scope, if present, else null. */ public function get_scorescope(): ?string { return $this->scorescope; } /** * Get all supported scopes for this service. * * @return string[] the array of supported scopes. */ public function get_scopes(): array { $scopes = []; foreach ($this->lineitemscopes as $lineitemscope) { $scopes[] = $lineitemscope; } if (!empty($this->resultscope)) { $scopes[] = $this->resultscope; } if (!empty($this->scorescope)) { $scopes[] = $this->scorescope; } return $scopes; } } classes/local/ltiadvantage/entity/context.php000064400000015650152266447110015466 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class context, instances of which represent a context in the platform. * * See: http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary for supported context types. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context { // The following full contexts are per the spec: // http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary. /** @var string course template context */ private const CONTEXT_TYPE_COURSE_TEMPLATE = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate'; /** @var string course offering context */ private const CONTEXT_TYPE_COURSE_OFFERING = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering'; /** @var string course section context */ private const CONTEXT_TYPE_COURSE_SECTION = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'; /** @var string group context */ private const CONTEXT_TYPE_GROUP = 'http://purl.imsglobal.org/vocab/lis/v2/course#Group'; // The following simple names are deprecated but are still supported in 1.3 for backwards compatibility. // http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary. /** @var string deprecated simple course template context */ private const LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE = 'CourseTemplate'; /** @var string deprecated simple course offering context */ private const LEGACY_CONTEXT_TYPE_COURSE_OFFERING = 'CourseOffering'; /** @var string deprecated simple course section context */ private const LEGACY_CONTEXT_TYPE_COURSE_SECTION = 'CourseSection'; /** @var string deprecated simple group context */ private const LEGACY_CONTEXT_TYPE_GROUP = 'Group'; /** @var int the local id of the deployment instance to which this context belongs. */ private $deploymentid; /** @var string the contextid as supplied by the platform. */ private $contextid; /** @var int|null the local id of this object instance, which can be null if the object hasn't been stored before */ private $id; /** @var string[] the array of context types */ private $types; /** * Private constructor. * * @param int $deploymentid the local id of the deployment instance to which this context belongs. * @param string $contextid the context id string, as provided by the platform during launch. * @param array $types an array of string context types, as provided by the platform during launch. * @param int|null $id local id of this object instance, nullable for new objects. */ private function __construct(int $deploymentid, string $contextid, array $types, ?int $id) { if (!is_null($id) && $id <= 0) { throw new \coding_exception('id must be a positive int'); } $this->deploymentid = $deploymentid; $this->contextid = $contextid; $this->set_types($types); // Handles type validation. $this->id = $id; } /** * Factory method for creating a context instance. * * @param int $deploymentid the local id of the deployment instance to which this context belongs. * @param string $contextid the context id string, as provided by the platform during launch. * @param array $types an array of string context types, as provided by the platform during launch. * @param int|null $id local id of this object instance, nullable for new objects. * @return context the context instance. */ public static function create(int $deploymentid, string $contextid, array $types, ?int $id = null): context { return new self($deploymentid, $contextid, $types, $id); } /** * Check whether a context is valid or not, checking also deprecated but supported legacy context names. * * @param string $type context type to check. * @param bool $includelegacy whether to check the legacy simple context names too. * @return bool true if the type is valid, false otherwise. */ private function is_valid_type(string $type, bool $includelegacy = false): bool { // Check LTI Advantage types. $valid = in_array($type, [ self::CONTEXT_TYPE_COURSE_TEMPLATE, self::CONTEXT_TYPE_COURSE_OFFERING, self::CONTEXT_TYPE_COURSE_SECTION, self::CONTEXT_TYPE_GROUP ]); // Check legacy short names. if ($includelegacy) { $valid = $valid || in_array($type, [ self::LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE, self::LEGACY_CONTEXT_TYPE_COURSE_OFFERING, self::LEGACY_CONTEXT_TYPE_COURSE_SECTION, self::LEGACY_CONTEXT_TYPE_GROUP ]); } return $valid; } /** * Get the object instance id. * * @return int|null the id, or null if the object doesn't yet have one assigned. */ public function get_id(): ?int { return $this->id; } /** * Return the platform contextid string. * * @return string the id of the context in the platform. */ public function get_contextid(): string { return $this->contextid; } /** * Get the id of the local deployment instance to which this context instance belongs. * * @return int the id of the local deployment instance to which this context instance belongs. */ public function get_deploymentid(): int { return $this->deploymentid; } /** * Get the context types this context instance represents. * * @return string[] the array of context types this context instance represents. */ public function get_types(): array { return $this->types; } /** * Set the list of types this context instance represents. * * @param array $types the array of string types. * @throws \coding_exception if any of the supplied types are invalid. */ public function set_types(array $types): void { foreach ($types as $type) { if (!$this->is_valid_type($type, true)) { throw new \coding_exception("Cannot set invalid context type '{$type}'."); } } $this->types = $types; } } classes/local/ltiadvantage/entity/application_registration.php000064400000030323152266447110021071 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class application_registration. * * This class represents an LTI Advantage Application Registration. * Each registered application may contain one or more deployments of the Moodle tool. * This registration provides the security contract for all tool deployments belonging to the registration. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class application_registration { /** @var int|null the if of this registration instance, or null if it hasn't been stored yet. */ private $id; /** @var string the name of the application being registered. */ private $name; /** @var \moodle_url the issuer identifying the platform, as provided by the platform. */ private $platformid; /** @var string the client id as provided by the platform. */ private $clientid; /** @var \moodle_url the authentication request URL, as provided by the platform. */ private $authenticationrequesturl; /** @var \moodle_url the certificate URL, as provided by the platform. */ private $jwksurl; /** @var \moodle_url the access token URL, as provided by the platform. */ private $accesstokenurl; /** @var string a unique identifier used by the registration in the initiate_login_uri to act as registration identifier.*/ private $uniqueid; /** @var int status of the registration, either incomplete (draft) or complete (all required data present). */ private $status; /** @var int const representing the incomplete state */ const REGISTRATION_STATUS_INCOMPLETE = 0; /** @var int const representing a complete state */ const REGISTRATION_STATUS_COMPLETE = 1; /** * The application_registration constructor. * * @param string $name the descriptor for this application registration. * @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI. * @param \moodle_url|null $platformid the URL of application * @param string|null $clientid unique id for the client on the application * @param \moodle_url|null $authenticationrequesturl URL to send OIDC Auth requests to. * @param \moodle_url|null $jwksurl URL to use to get public keys from the application. * @param \moodle_url|null $accesstokenurl URL to use to get an access token from the application, used in service calls. * @param int|null $id the id of the object instance, if being created from an existing store item. */ private function __construct(string $name, string $uniqueid, ?\moodle_url $platformid, ?string $clientid, ?\moodle_url $authenticationrequesturl, ?\moodle_url $jwksurl, ?\moodle_url $accesstokenurl, ?int $id = null) { if (empty($name)) { throw new \coding_exception("Invalid 'name' arg. Cannot be an empty string."); } if (empty($uniqueid)) { throw new \coding_exception("Invalid 'uniqueid' arg. Cannot be an empty string."); } // Resolve the registration status. $iscomplete = (!is_null($platformid) && !is_null($clientid) && !is_null($authenticationrequesturl) && !is_null($authenticationrequesturl) && !is_null($jwksurl) && !is_null($accesstokenurl)); $this->status = $iscomplete ? self::REGISTRATION_STATUS_COMPLETE : self::REGISTRATION_STATUS_INCOMPLETE; $this->name = $name; $this->uniqueid = $uniqueid; $this->platformid = $platformid; $this->clientid = $clientid; $this->authenticationrequesturl = $authenticationrequesturl; $this->jwksurl = $jwksurl; $this->accesstokenurl = $accesstokenurl; $this->id = $id; } /** * Factory method to create a new instance of an application registration * * @param string $name the descriptor for this application registration. * @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI. * @param \moodle_url $platformid the URL of application * @param string $clientid unique id for the client on the application * @param \moodle_url $authenticationrequesturl URL to send OIDC Auth requests to. * @param \moodle_url $jwksurl URL to use to get public keys from the application. * @param \moodle_url $accesstokenurl URL to use to get an access token from the application, used in service calls. * @param int|null $id the id of the object instance, if being created from an existing store item. * @return application_registration the application_registration instance. * @throws \coding_exception if an invalid clientid is provided. */ public static function create(string $name, string $uniqueid, \moodle_url $platformid, string $clientid, \moodle_url $authenticationrequesturl, \moodle_url $jwksurl, \moodle_url $accesstokenurl, ?int $id = null): application_registration { if (empty($clientid)) { throw new \coding_exception("Invalid 'clientid' arg. Cannot be an empty string."); } return new self($name, $uniqueid, $platformid, $clientid, $authenticationrequesturl, $jwksurl, $accesstokenurl, $id); } /** * Factory method to create a draft application registration. * * @param string $name the descriptor for the draft application registration. * @param string $uniqueid a unique identifier for the registration used in place of client_id in the login URI. * @param int|null $id the id of the object instance, if being created from an existing store item. * @return application_registration the application_registration instance. */ public static function create_draft(string $name, string $uniqueid, ?int $id = null): application_registration { return new self($name, $uniqueid, null, null, null, null, null, $id); } /** * Get the integer id of this object instance. * * Will return null if the instance has not yet been stored. * * @return null|int the id, if set, otherwise null. */ public function get_id(): ?int { return $this->id; } /** * Get the name of the application being registered. * * @return string the name. */ public function get_name(): string { return $this->name; } /** * Sets the name of this registration. * * @param string $name the new name to set. * @throws \coding_exception if the provided name is invalid. */ public function set_name(string $name): void { if (empty($name)) { throw new \coding_exception("Invalid 'name' arg. Cannot be an empty string."); } $this->name = $name; } /** * Return the local unique client id of the registration. * * @return string the id. */ public function get_uniqueid(): string { return $this->uniqueid; } /** * Get the platform id. * * @return \moodle_url|null the platformid/issuer URL. */ public function get_platformid(): ?\moodle_url { return $this->platformid; } /** * Sets the platformid/issuer for this registration. * * @param \moodle_url $platformid the platform id / iss to set. */ public function set_platformid(\moodle_url $platformid): void { $this->platformid = $platformid; } /** * Get the client id. * * @return string|null the client id. */ public function get_clientid(): ?string { return $this->clientid; } /** * Sets the client id for this registration. * * @param string $clientid the client id * @throws \coding_exception if the client id is invalid. */ public function set_clientid(string $clientid): void { if (empty($clientid)) { throw new \coding_exception("Invalid 'clientid' arg. Cannot be an empty string."); } $this->clientid = $clientid; } /** * Get the authentication request URL. * * @return \moodle_url|null the authentication request URL. */ public function get_authenticationrequesturl(): ?\moodle_url { return $this->authenticationrequesturl; } /** * Sets the authentication request URL for this registration. * * @param \moodle_url $authenticationrequesturl the authentication request URL. */ public function set_authenticationrequesturl(\moodle_url $authenticationrequesturl): void { $this->authenticationrequesturl = $authenticationrequesturl; } /** * Get the JWKS URL. * * @return \moodle_url|null the JWKS URL. */ public function get_jwksurl(): ?\moodle_url { return $this->jwksurl; } /** * Sets the JWKS URL for this registration. * * @param \moodle_url $jwksurl the JWKS URL. */ public function set_jwksurl(\moodle_url $jwksurl): void { $this->jwksurl = $jwksurl; } /** * Get the access token URL. * * @return \moodle_url|null the access token URL. */ public function get_accesstokenurl(): ?\moodle_url { return $this->accesstokenurl; } /** * Sets the access token URL for this registration. * * @param \moodle_url $accesstokenurl the access token URL. */ public function set_accesstokenurl(\moodle_url $accesstokenurl): void { $this->accesstokenurl = $accesstokenurl; } /** * Add a tool deployment to this registration. * * @param string $name human readable name for the deployment. * @param string $deploymentid the unique id of the tool deployment in the platform. * @return deployment the new deployment. * @throws \coding_exception if trying to add a deployment to an instance without an id assigned. */ public function add_tool_deployment(string $name, string $deploymentid): deployment { if (empty($this->get_id())) { throw new \coding_exception("Can't add deployment to a resource_link that hasn't first been saved."); } return deployment::create( $this->get_id(), $deploymentid, $name ); } /** * Check whether this registration is complete or not. */ public function is_complete(): bool { return $this->status == self::REGISTRATION_STATUS_COMPLETE; } /** * Attempt to progress this registration to the 'complete' state, provided required state exists. * * @see REGISTRATION_STATUS_COMPLETE * * @throws \coding_exception if the registration isn't in a state to be transitioned to complete. */ public function complete_registration(): void { // Check completeness of registration. if (is_null($this->platformid)) { throw new \coding_exception("Unable to complete registration. Platform ID is missing."); } if (is_null($this->clientid)) { throw new \coding_exception("Unable to complete registration. Client ID is missing."); } if (is_null($this->accesstokenurl)) { throw new \coding_exception("Unable to complete registration. Access token URL is missing."); } if (is_null($this->authenticationrequesturl)) { throw new \coding_exception("Unable to complete registration. Authentication request URL is missing."); } if (is_null($this->jwksurl)) { throw new \coding_exception("Unable to complete registration. JWKS URL is missing."); } $this->status = self::REGISTRATION_STATUS_COMPLETE; } } classes/local/ltiadvantage/entity/migration_claim.php000064400000017716152266447110017145 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; /** * The migration_claim class, instances of which represent information passed in an 'lti1p1' migration claim. * * Provides validation and data retrieval for the claim. * * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class migration_claim { /** @var string the LTI 1.1 consumer key */ private $consumerkey; /** @var string the LTI 1.1 user identifier. * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim. * I.e. https://www.imsglobal.org/spec/security/v1p0#id-token */ private $userid = null; /** @var string the LTI 1.1 context identifier. * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'context' claim. * I.e. https://purl.imsglobal.org/spec/lti/claim/context#id. */ private $contextid = null; /** @var string the LTI 1.1 consumer instance GUID. * This is only included in the claim if it differs to the 'guid' property of the LTI 1.3 'tool_platform' claim. * I.e. https://purl.imsglobal.org/spec/lti/claim/tool_platform#guid. */ private $toolconsumerinstanceguid = null; /** @var string the LTI 1.1 resource link identifier. * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'resource_link' claim. * I.e. https://purl.imsglobal.org/spec/lti/claim/resource_link#id. */ private $resourcelinkid = null; /** @var legacy_consumer_repository repository instance for querying consumer secrets when verifying signature. */ private $legacyconsumerrepo; /** * The migration_claim constructor. * * @param array $claim the array of claim data, as received in a resource link launch. * @param string $deploymentid the deployment id included in the launch. * @param string $platform the platform included in the launch. * @param string $clientid the client id included in the launch. * @param string $exp the exp included in the launch. * @param string $nonce the nonce included in the launch. * @param legacy_consumer_repository $legacyconsumerrepo a legacy consumer repository instance. * @throws \coding_exception if the claim data is invalid. */ public function __construct(array $claim, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce, legacy_consumer_repository $legacyconsumerrepo) { // The oauth_consumer_key property MUST be sent. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key. if (empty($claim['oauth_consumer_key'])) { throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim."); } // The oauth_consumer_key_sign property MAY be sent. // For user migration to take place, however, this is deemed a required property. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign. if (empty($claim['oauth_consumer_key_sign'])) { throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."); } $this->legacyconsumerrepo = $legacyconsumerrepo; if (!$this->verify_signature($claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $deploymentid, $platform, $clientid, $exp, $nonce, $legacyconsumerrepo)) { throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim."); } $this->consumerkey = $claim['oauth_consumer_key']; $this->userid = $claim['user_id'] ?? null; $this->contextid = $claim['context_id'] ?? null; $this->toolconsumerinstanceguid = $claim['tool_consumer_instance_guid'] ?? null; $this->resourcelinkid = $claim['resource_link_id'] ?? null; } /** * Verify the claim signature by recalculating it using the launch data and locally stored consumer secret. * * @param string $consumerkey the LTI 1.1 consumer key. * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data. * @param string $deploymentid the deployment id included in the launch. * @param string $platform the platform included in the launch. * @param string $clientid the client id included in the launch. * @param string $exp the exp included in the launch. * @param string $nonce the nonce included in the launch. * @return bool true if the signature was verified, false otherwise. */ private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce): bool { $base = [ $consumerkey, $deploymentid, $platform, $clientid, $exp, $nonce ]; $basestring = implode('&', $base); // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found // or no signatures match. $consumersecrets = $this->legacyconsumerrepo->get_consumer_secrets($consumerkey); foreach ($consumersecrets as $consumersecret) { $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret)); if ($signature === $calculatedsignature) { return true; } } return false; } /** * Return the consumer key stored in the claim. * * @return string the consumer key included in the claim. */ public function get_consumer_key(): string { return $this->consumerkey; } /** * Return the LTI 1.1 user id stored in the claim. * * @return string|null the user id, or null if not provided in the claim. */ public function get_user_id(): ?string { return $this->userid; } /** * Return the LTI 1.1 context id stored in the claim. * * @return string|null the context id, or null if not provided in the claim. */ public function get_context_id(): ?string { return $this->contextid; } /** * Return the LTI 1.1 tool consumer instance GUID stored in the claim. * * @return string|null the tool consumer instance GUID, or null if not provided in the claim. */ public function get_tool_consumer_instance_guid(): ?string { return $this->toolconsumerinstanceguid; } /** * Return the LTI 1.1 resource link id stored in the claim. * * @return string|null the resource link id, or null if not provided in the claim. */ public function get_resource_link_id(): ?string { return $this->resourcelinkid; } } classes/local/ltiadvantage/entity/resource_link.php000064400000022215152266447110016641 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class resource_link. * * This class represents an LTI Advantage Resource Link (http://www.imsglobal.org/spec/lti/v1p3/#resource-link). * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class resource_link { /** @var int|null the id of this object, or null if the object hasn't been stored yet. */ private $id; /** @var string resourcelinkid the id of the resource link as supplied by the platform. */ private $resourcelinkid; /** @var int $deploymentid the local id of the deployment instance to which this resource link belongs. */ private $deploymentid; /** @var int|null $contextid the id of local context object representing the platform context, or null. */ private $contextid; /** @var int The id of the local published resource this resource_link points to. */ private $resourceid; /** @var ags_info|null the grade service for this resource_link, null if not applicable/not provided. */ private $gradeservice; /** @var nrps_info|null the names and roles service for this resource_link, null if not applicable/not provided. */ private $namesrolesservice; /** * The private resource_link constructor. * * @param string $resourcelinkid the id of the resource link as supplied by the platform. * @param int $deploymentid the local id of the deployment instance to which this resource link belongs. * @param int $resourceid the id of the local resource to which this link refers. * @param int|null $contextid the id local context object representing the context within the platform. * @param int|null $id the local id of this resource_link object. * @throws \coding_exception if the instance is unable to be created. */ private function __construct(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null, ?int $id = null) { if (empty($resourcelinkid)) { throw new \coding_exception('Error: resourcelinkid cannot be an empty string'); } $this->resourcelinkid = $resourcelinkid; $this->deploymentid = $deploymentid; $this->resourceid = $resourceid; $this->contextid = $contextid; $this->id = $id; $this->gradeservice = null; $this->namesrolesservice = null; } /** * Factory method to create an instance. * * @param string $resourcelinkid the resourcelinkid, as provided by the platform. * @param int $deploymentid the local id of the deployment to which this resource link belongs. * @param int $resourceid the id of the local resource this resource_link refers to. * @param int|null $contextid the id of the local context object representing the platform context. * @param int|null $id the local id of the resource link instance. * @return resource_link the newly created instance. */ public static function create(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null, ?int $id = null): resource_link { return new self($resourcelinkid, $deploymentid, $resourceid, $contextid, $id); } /** * Return the id of this object instance. * * @return null|int the id or null if the object has not yet been stored. */ public function get_id(): ?int { return $this->id; } /** * Get the resourcelinkid as provided by the platform. * * @return string the resourcelinkid. */ public function get_resourcelinkid(): string { return $this->resourcelinkid; } /** * Return the id of the deployment to which this resource link belongs. * * This id is the local id of the deployment instance, not the deploymentid provided by the platform. * * @return int the deployment id. */ public function get_deploymentid(): int { return $this->deploymentid; } /** * Get the local id of the published resource to which this resource link refers. * * @return int the id of the published resource. */ public function get_resourceid(): int { return $this->resourceid; } /** * Return the id of the context object holding information about where this resource link resides. * * @return int|null the id or null if not present. */ public function get_contextid(): ?int { return $this->contextid; } /** * Link this resource_link instance with a context. * * @param int $contextid the local id of the context instance containing information about the platform context. */ public function set_contextid(int $contextid): void { if ($contextid <= 0) { throw new \coding_exception('Context id must be a positive int'); } $this->contextid = $contextid; } /** * Set which local published resource this resource link refers to. * * @param int $resourceid the published resource id. */ public function set_resourceid(int $resourceid): void { if ($resourceid <= 0) { throw new \coding_exception('Resource id must be a positive int'); } $this->resourceid = $resourceid; } /** * Add grade service information to this resource_link instance. * * @param \moodle_url|null $lineitemsurl the service URL for get/put of line items, if supported. * @param \moodle_url|null $lineitemurl the service URL if only a single line item is present in the platform. * @param string[] $scopes the string array of grade service scopes which may be used by the service. */ public function add_grade_service(?\moodle_url $lineitemsurl = null, ?\moodle_url $lineitemurl = null, array $scopes = []) { $this->gradeservice = ags_info::create($lineitemsurl, $lineitemurl, $scopes); } /** * Get the grade service attached to this resource_link instance, or null if there isn't one associated. * * @return ags_info|null the grade service object instance, or null if not found. */ public function get_grade_service(): ?ags_info { return $this->gradeservice; } /** * Add names and roles service information to this resource_link instance. * * @param \moodle_url $contextmembershipurl the service URL for memberships. * @param string[] $serviceversions the string array of supported service versions. */ public function add_names_and_roles_service(\moodle_url $contextmembershipurl, array $serviceversions): void { $this->namesrolesservice = nrps_info::create($contextmembershipurl, $serviceversions); } /** * Get the names and roles service attached to this resource_link instance, or null if there isn't one associated. * * @return nrps_info|null the names and roles service object instance, or null if not found. */ public function get_names_and_roles_service(): ?nrps_info { return $this->namesrolesservice; } /** * Factory method to create a user from this resource_link instance. * * This is useful for associating the user with the resource link and resource I.e. the user was created when * launching a specific resource link. * * @param int $userid the id of the Moodle user record. * @param string $sourceid the id of the user on the platform. * @param string $lang the user's lang code. * @param string $city the user's city. * @param string $country the user's country. * @param string $institution the user's institution. * @param string $timezone the user's timezone. * @param int|null $maildisplay the user's maildisplay, which can be omitted to use sensible defaults. * @return user the user instance. * @throws \coding_exception if trying to add a user to an as-yet-unsaved resource_link instance. */ public function add_user(int $userid, string $sourceid, string $lang, string $city, string $country, string $institution, string $timezone, ?int $maildisplay = null): user { if (empty($this->get_id())) { throw new \coding_exception('Can\'t add user to a resource_link that hasn\'t first been saved'); } return user::create_from_resource_link($this->get_id(), $this->get_resourceid(), $userid, $this->get_deploymentid(), $sourceid, $lang, $timezone, $city, $country, $institution, $maildisplay); } } classes/local/ltiadvantage/entity/deployment.php000064400000015616152266447110016164 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class deployment. * * This class represents an LTI Advantage Tool Deployment (http://www.imsglobal.org/spec/lti/v1p3/#tool-deployment). * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class deployment { /** @var int|null the id of this object instance, or null if it has not been saved yet. */ private $id; /** @var string the name of this deployment. */ private $deploymentname; /** @var string The platform-issued deployment id. */ private $deploymentid; /** @var int the local ID of the application registration to which this deployment belongs. */ private $registrationid; /** @var string|null the legacy consumer key, if the deployment instance is migrated from a legacy consumer. */ private $legacyconsumerkey; /** * The private deployment constructor. * * @param string $deploymentname the name of this deployment. * @param string $deploymentid the platform-issued deployment id. * @param int $registrationid the local ID of the application registration. * @param int|null $id the id of this object instance, or null if it is a new instance which has not yet been saved. * @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades. */ private function __construct(string $deploymentname, string $deploymentid, int $registrationid, ?int $id = null, ?string $legacyconsumerkey = null) { if (!is_null($id) && $id <= 0) { throw new \coding_exception('id must be a positive int'); } if (empty($deploymentname)) { throw new \coding_exception("Invalid 'deploymentname' arg. Cannot be an empty string."); } if (empty($deploymentid)) { throw new \coding_exception("Invalid 'deploymentid' arg. Cannot be an empty string."); } $this->deploymentname = $deploymentname; $this->deploymentid = $deploymentid; $this->registrationid = $registrationid; $this->id = $id; $this->legacyconsumerkey = $legacyconsumerkey; } /** * Factory method to create a new instance of a deployment. * * @param int $registrationid the local ID of the application registration. * @param string $deploymentid the platform-issued deployment id. * @param string $deploymentname the name of this deployment. * @param int|null $id optional local id of this object instance, omitted for new deployment objects. * @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades. * @return deployment the deployment instance. */ public static function create(int $registrationid, string $deploymentid, string $deploymentname, ?int $id = null, ?string $legacyconsumerkey = null): deployment { return new self($deploymentname, $deploymentid, $registrationid, $id, $legacyconsumerkey); } /** * Return the object id. * * @return int|null the id. */ public function get_id(): ?int { return $this->id; } /** * Return the short name of this tool deployment. * * @return string the short name. */ public function get_deploymentname(): string { return $this->deploymentname; } /** * Get the deployment id string. * * @return string deploymentid */ public function get_deploymentid(): string { return $this->deploymentid; } /** * Get the id of the application_registration. * * @return int the id of the application_registration instance to which this deployment belongs. */ public function get_registrationid(): int { return $this->registrationid; } /** * Get the legacy consumer key to which this deployment instance is mapped. * * @return string|null the legacy consumer key, if set, else null. */ public function get_legacy_consumer_key(): ?string { return $this->legacyconsumerkey; } /** * Factory method to add a platform-specific context to the deployment. * * @param string $contextid the contextid, as supplied by the platform during launch. * @param array $types the context types the context represents, as supplied by the platform during launch. * @return context the context instance. * @throws \coding_exception if the context could not be created. */ public function add_context(string $contextid, array $types): context { if (!$this->get_id()) { throw new \coding_exception('Can\'t add context to a deployment that hasn\'t first been saved'); } return context::create($this->get_id(), $contextid, $types); } /** * Factory method to create a resource link from this deployment instance. * * @param string $resourcelinkid the platform-issued string id of the resource link. * @param int $resourceid the local published resource to which this link points. * @param int|null $contextid the platform context instance in which the resource link resides, if available. * @return resource_link the resource_link instance. * @throws \coding_exception if the resource_link can't be created. */ public function add_resource_link(string $resourcelinkid, int $resourceid, ?int $contextid = null): resource_link { if (!$this->get_id()) { throw new \coding_exception('Can\'t add resource_link to a deployment that hasn\'t first been saved'); } return resource_link::create($resourcelinkid, $this->get_id(), $resourceid, $contextid); } /** * Set the legacy consumer key for this instance, indicating that the deployment has been migrated from a consumer. * * @param string $key the legacy consumer key. * @throws \coding_exception if the key is invalid. */ public function set_legacy_consumer_key(string $key): void { if (strlen($key) > 255) { throw new \coding_exception('Legacy consumer key too long. Cannot exceed 255 chars.'); } $this->legacyconsumerkey = $key; } } classes/local/ltiadvantage/entity/user.php000064400000036253152266447110014762 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class user, instances of which represent a specific lti user in the tool. * * A user is always associated with a resource, as lti users cannot exist without a tool-published-resource. A user will * always come from either: * - a resource link launch or * - a membership sync * Both of which required a published resource. * * Additionally, a user may be associated with a given resource_link instance, to signify that the user originated from * that resource_link. If a user is not associated with a resource_link, such as when creating a user during a member * sync, that param is nullable. This can be achieved via the factory method user::create_from_resource_link() or set * after instantiation via the user::set_resource_link_id() method. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user { /** @var int the id of the published resource to which this user belongs. */ private $resourceid; /** @var int the local id of the deployment instance to which this user belongs. */ private $deploymentid; /** @var string the id of the user in the platform site. */ private $sourceid; /** @var int|null the id of this user instance, or null if not stored yet. */ private $id; /** @var int|null the id of the user in the tool site, or null if the instance hasn't been stored yet. */ private $localid; /** @var string city of the user. */ private $city; /** @var string country of the user. */ private $country; /** @var string institution of the user.*/ private $institution; /** @var string timezone of the user. */ private $timezone; /** @var int maildisplay of the user. */ private $maildisplay; /** @var string language code of the user. */ private $lang; /** @var float the user's last grade value. */ private $lastgrade; /** @var int|null the user's last access unix timestamp, or null if they have not accessed the resource yet.*/ private $lastaccess; /** @var int|null the id of the resource_link instance, or null if the user doesn't originate from one. */ private $resourcelinkid; /** * Private constructor. * * @param int $resourceid the id of the published resource to which this user belongs. * @param int $userid the id of the Moodle user to which this LTI user relates. * @param int $deploymentid the local id of the deployment instance to which this user belongs. * @param string $sourceid the id of the user in the platform site. * @param string $lang the user's language code. * @param string $city the user's city. * @param string $country the user's country. * @param string $institution the user's institution. * @param string $timezone the user's timezone. * @param int|null $maildisplay the user's maildisplay, or null to select defaults. * @param float|null $lastgrade the user's last grade value. * @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource. * @param int|null $resourcelinkid the id of the resource link to link to the user, or null if not applicable. * @param int|null $id the id of this object instance, or null if it's a not-yet-persisted object. * @throws \coding_exception */ private function __construct(int $resourceid, int $userid, int $deploymentid, string $sourceid, string $lang, string $city, string $country, string $institution, string $timezone, ?int $maildisplay, ?float $lastgrade, ?int $lastaccess, ?int $resourcelinkid = null, ?int $id = null) { global $CFG; $this->resourceid = $resourceid; $this->localid = $userid; $this->deploymentid = $deploymentid; if (empty($sourceid)) { throw new \coding_exception('Invalid sourceid value. Cannot be an empty string.'); } $this->sourceid = $sourceid; $this->set_lang($lang); $this->set_city($city); $this->set_country($country); $this->set_institution($institution); $this->set_timezone($timezone); $maildisplay = $maildisplay ?? ($CFG->defaultpreference_maildisplay ?? 2); $this->set_maildisplay($maildisplay); $this->lastgrade = $lastgrade ?? 0.0; $this->lastaccess = $lastaccess; $this->resourcelinkid = $resourcelinkid; $this->id = $id; } /** * Factory method for creating a user instance associated with a given resource_link instance. * * @param int $resourcelinkid the local id of the resource link instance to link to the user. * @param int $resourceid the id of the published resource to which this user belongs. * @param int $userid the id of the Moodle user to which this LTI user relates. * @param int $deploymentid the local id of the deployment instance to which this user belongs. * @param string $sourceid the id of the user in the platform site. * @param string $lang the user's language code. * @param string $timezone the user's timezone. * @param string $city the user's city. * @param string $country the user's country. * @param string $institution the user's institution. * @param int|null $maildisplay the user's maildisplay, or null to select defaults. * @return user the user instance. */ public static function create_from_resource_link(int $resourcelinkid, int $resourceid, int $userid, int $deploymentid, string $sourceid, string $lang, string $timezone, string $city = '', string $country = '', string $institution = '', ?int $maildisplay = null): user { return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city, $country, $institution, $timezone, $maildisplay, null, null, $resourcelinkid); } /** * Factory method for creating a user. * * @param int $resourceid the id of the published resource to which this user belongs. * @param int $userid the id of the Moodle user to which this LTI user relates. * @param int $deploymentid the local id of the deployment instance to which this user belongs. * @param string $sourceid the id of the user in the platform site. * @param string $lang the user's language code. * @param string $timezone the user's timezone. * @param string $city the user's city. * @param string $country the user's country. * @param string $institution the user's institution. * @param int|null $maildisplay the user's maildisplay, or null to select defaults. * @param float|null $lastgrade the user's last grade value. * @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource. * @param int|null $resourcelinkid the local id of the resource link instance associated with the user. * @param int|null $id the id of this lti user instance, or null if it's a not-yet-persisted object. * @return user the user instance. */ public static function create(int $resourceid, int $userid, int $deploymentid, string $sourceid, string $lang, string $timezone, string $city = '', string $country = '', string $institution = '', ?int $maildisplay = null, ?float $lastgrade = null, ?int $lastaccess = null, ?int $resourcelinkid = null, ?int $id = null): user { return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city, $country, $institution, $timezone, $maildisplay, $lastgrade, $lastaccess, $resourcelinkid, $id); } /** * Get the id of this user instance. * * @return int|null the object id, or null if not yet persisted. */ public function get_id(): ?int { return $this->id; } /** * Get the id of the resource_link instance to which this user is associated. * * @return int|null the object id, or null if the user isn't associated with one. */ public function get_resourcelinkid(): ?int { return $this->resourcelinkid; } /** * Associate this user with the given resource_link instance, denoting that this user launched from the instance. * * @param int $resourcelinkid the id of the resource_link instance. */ public function set_resourcelinkid(int $resourcelinkid): void { if ($resourcelinkid <= 0) { throw new \coding_exception("Invalid resourcelinkid '$resourcelinkid' provided. Must be > 0."); } $this->resourcelinkid = $resourcelinkid; } /** * Get the id of the published resource to which this user is associated. * * @return int the resource id. */ public function get_resourceid(): int { return $this->resourceid; } /** * Get the id of the deployment instance to which this user belongs. * * @return int id of the deployment instance. */ public function get_deploymentid(): int { return $this->deploymentid; } /** * Get the id of the user in the platform. * * @return string the source id. */ public function get_sourceid(): string { return $this->sourceid; } /** * Get the id of the user in the tool. * * @return int|null the id, or null if the object instance hasn't yet been persisted. */ public function get_localid(): ?int { return $this->localid; } /** * Get the user's city. * * @return string the city. */ public function get_city(): string { return $this->city; } /** * Set the user's city. * * @param string $city the city string. */ public function set_city(string $city): void { $this->city = $city; } /** * Get the user's country code. * * @return string the country code. */ public function get_country(): string { return $this->country; } /** * Set the user's country. * * @param string $countrycode the 2 digit country code representing the country, or '' to denote none. */ public function set_country(string $countrycode): void { global $CFG; require_once($CFG->libdir . '/moodlelib.php'); $validcountrycodes = array_merge([''], array_keys(get_string_manager()->get_list_of_countries(true))); if (!in_array($countrycode, $validcountrycodes)) { throw new \coding_exception("Invalid country code '$countrycode'."); } $this->country = $countrycode; } /** * Get the instituation of the user. * * @return string the institution. */ public function get_institution(): string { return $this->institution; } /** * Set the user's institution. * * @param string $institution the name of the institution. */ public function set_institution(string $institution): void { $this->institution = $institution; } /** * Get the timezone of the user. * * @return string the user timezone. */ public function get_timezone(): string { return $this->timezone; } /** * Set the user's timezone, or set '99' to specify server timezone. * * @param string $timezone the timezone string, or '99' to use server timezone. */ public function set_timezone(string $timezone): void { if (empty($timezone)) { throw new \coding_exception('Invalid timezone value. Cannot be an empty string.'); } $validtimezones = array_keys(\core_date::get_list_of_timezones(null, true)); if (!in_array($timezone, $validtimezones)) { throw new \coding_exception("Invalid timezone '$timezone' provided."); } $this->timezone = $timezone; } /** * Get the maildisplay of the user. * * @return int the maildisplay. */ public function get_maildisplay(): int { return $this->maildisplay; } /** * Set the user's mail display preference from a range of supported options. * * 0 - hide from non privileged users * 1 - allow everyone to see * 2 - allow only course participants to see * * @param int $maildisplay the maildisplay preference to set. */ public function set_maildisplay(int $maildisplay): void { if (!in_array($maildisplay, range(0, 2))) { throw new \coding_exception("Invalid maildisplay value '$maildisplay'. Must be in the range {0..2}."); } $this->maildisplay = $maildisplay; } /** * Get the lang code of the user. * * @return string the user's language code. */ public function get_lang(): string { return $this->lang; } /** * Set the user's language. * * @param string $langcode the language code representing the user's language. */ public function set_lang(string $langcode): void { if (empty($langcode)) { throw new \coding_exception('Invalid lang value. Cannot be an empty string.'); } $validlangcodes = array_keys(get_string_manager()->get_list_of_translations()); if (!in_array($langcode, $validlangcodes)) { throw new \coding_exception("Invalid lang '$langcode' provided."); } $this->lang = $langcode; } /** * Get the last grade value for this user. * * @return float the float grade. */ public function get_lastgrade(): float { return $this->lastgrade; } /** * Set the last grade for the user. * * @param float $lastgrade the last grade the user received. */ public function set_lastgrade(float $lastgrade): void { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $this->lastgrade = grade_floatval($lastgrade); } /** * Get the last access timestamp for this user. * * @return int|null the last access timestampt, or null if the user hasn't accessed the resource. */ public function get_lastaccess(): ?int { return $this->lastaccess; } /** * Set the last access time for the user. * * @param int $time unix timestamp representing the last time the user accessed the published resource. * @throws \coding_exception if $time is not a positive int. */ public function set_lastaccess(int $time): void { if ($time < 0) { throw new \coding_exception('Cannot set negative access time'); } $this->lastaccess = $time; } } classes/local/ltiadvantage/entity/nrps_info.php000064400000011450152266447110015771 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Class nrps_info, instances of which represent a names and roles provisioning service for a resource. * * For information about Names and Role Provisioning Services 2.0, see http://www.imsglobal.org/spec/lti-nrps/v2p0. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class nrps_info { /** @var \moodle_url the memberships URL for the service. */ private $contextmembershipsurl; /** @var float[] the array of supported service versions. */ private $serviceversions; // Service versions are specified by the platform during launch. // See http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration. /** @var string version 1.0 */ private const SERVICE_VERSION_1 = '1.0'; /** @var string version 2.0 */ private const SERVICE_VERSION_2 = '2.0'; // Scope that must be requested as part of making a service call. // See: http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration. /** @var string the scope to request to make service calls. */ private $servicescope = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; /** * The private nrps_info constructor. * * @param \moodle_url $contextmembershipsurl the memberships URL. * @param string[] $serviceversions the supported service versions. */ private function __construct(\moodle_url $contextmembershipsurl, array $serviceversions = [self::SERVICE_VERSION_2]) { $this->contextmembershipsurl = $contextmembershipsurl; $this->set_service_versions($serviceversions); } /** * Factory method to create a new nrps_info instance. * * @param \moodle_url $contextmembershipsurl the memberships URL. * @param string[] $serviceversions the supported service versions. * @return nrps_info the object instance. */ public static function create(\moodle_url $contextmembershipsurl, array $serviceversions = [self::SERVICE_VERSION_2]): nrps_info { return new self($contextmembershipsurl, $serviceversions); } /** * Check whether the supplied service version is valid or not. * * @param string $serviceversion the service version to check. * @return bool true if valid, false otherwise. */ private function is_valid_service_version(string $serviceversion): bool { $validversions = [ self::SERVICE_VERSION_1, self::SERVICE_VERSION_2 ]; return in_array($serviceversion, $validversions); } /** * Tries to set the supported service versions for this instance. * * @param array $serviceversions the service versions to set. * @throws \coding_exception if any of the supplied versions are not valid. */ private function set_service_versions(array $serviceversions): void { if (empty($serviceversions)) { throw new \coding_exception('Service versions array cannot be empty'); } $serviceversions = array_unique($serviceversions); foreach ($serviceversions as $serviceversion) { if (!$this->is_valid_service_version($serviceversion)) { throw new \coding_exception("Invalid Names and Roles service version '{$serviceversion}'"); } } $this->serviceversions = $serviceversions; } /** * Get the service URL for this grade service instance. * * @return \moodle_url the service URL. */ public function get_context_memberships_url(): \moodle_url { return clone $this->contextmembershipsurl; } /** * Get the supported service versions for this grade service instance. * * @return string[] the array of supported service versions. */ public function get_service_versions(): array { return $this->serviceversions; } /** * Get the nrps service scope. * * @return string the service scope. */ public function get_service_scope(): string { return $this->servicescope; } } classes/local/ltiadvantage/service/application_registration_service.php000064400000016037152266447110022743 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; /** * Class application_registration_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class application_registration_service { /** @var application_registration_repository repository to work with application_registration instances. */ private $appregistrationrepo; /** @var deployment_repository repository to work with deployment instances. */ private $deploymentrepo; /** @var resource_link_repository repository to work with resource link instances. */ private $resourcelinkrepo; /** @var context_repository repository to work with context instances. */ private $contextrepo; /** @var user_repository repository to work with user instances. */ private $userrepo; /** * The application_registration_service constructor. * * @param application_registration_repository $appregistrationrepo an application registration repository instance. * @param deployment_repository $deploymentrepo a deployment repository instance. * @param resource_link_repository $resourcelinkrepo a resource_link_repository instance. * @param context_repository $contextrepo a context_repository instance. * @param user_repository $userrepo a user_repository instance. */ public function __construct(application_registration_repository $appregistrationrepo, deployment_repository $deploymentrepo, resource_link_repository $resourcelinkrepo, context_repository $contextrepo, user_repository $userrepo) { $this->appregistrationrepo = $appregistrationrepo; $this->deploymentrepo = $deploymentrepo; $this->resourcelinkrepo = $resourcelinkrepo; $this->contextrepo = $contextrepo; $this->userrepo = $userrepo; } /** * Convert a DTO into a new application_registration domain object. * * @param \stdClass $dto the object containing information needed to register an application. * @return application_registration the application_registration object */ private function registration_from_dto(\stdClass $dto): application_registration { $registration = $this->appregistrationrepo->find($dto->id); $registration->set_name($dto->name); $registration->set_platformid(new \moodle_url($dto->platformid)); $registration->set_clientid($dto->clientid); $registration->set_accesstokenurl(new \moodle_url($dto->accesstokenurl)); $registration->set_jwksurl(new \moodle_url($dto->jwksurl)); $registration->set_authenticationrequesturl(new \moodle_url($dto->authenticationrequesturl)); $registration->complete_registration(); return $registration; } /** * Gets a unique id for the registration, with uniqueness guaranteed with a lookup. * * @return string the unique id. */ private function get_unique_id(): string { global $DB; do { $bytes = random_bytes(30); $uniqueid = bin2hex($bytes); } while ($DB->record_exists('enrol_lti_app_registration', ['uniqueid' => $uniqueid])); return $uniqueid; } /** * Convert a DTO into a new DRAFT application_registration domain object. * * @param \stdClass $dto the object containing information needed to create the draft registration. * @return application_registration the draft application_registration object */ private function draft_registration_from_dto(\stdClass $dto): application_registration { return application_registration::create_draft( $dto->name, $this->get_unique_id() ); } /** * Application service handling the use case "As an admin I can create a draft platform registration". * * @param \stdClass $appregdto details of the draft application to create. * @return application_registration the application_registration domain object. * @throws \coding_exception if the DTO doesn't contain required fields. */ public function create_draft_application_registration(\stdClass $appregdto): application_registration { if (empty($appregdto->name)) { throw new \coding_exception('Cannot create draft registration. Name is missing.'); } $draftregistration = $this->draft_registration_from_dto($appregdto); return $this->appregistrationrepo->save($draftregistration); } /** * Application service handling the use case "As an admin I can update the registration of an LTI platform". * * @param \stdClass $appregdto details of the registration to update. * @return application_registration the application_registration domain object. */ public function update_application_registration(\stdClass $appregdto): application_registration { if (empty($appregdto->id)) { throw new \coding_exception('Cannot update registration. Id is missing.'); } return $this->appregistrationrepo->save($this->registration_from_dto($appregdto)); } /** * Application service handling the use case "As an admin I can delete a registration of an LTI platform". * * @param int $registrationid id of the registration to delete. */ public function delete_application_registration(int $registrationid): void { $deployments = $this->deploymentrepo->find_all_by_registration($registrationid); if ($deployments) { $deploymentservice = new tool_deployment_service( $this->appregistrationrepo, $this->deploymentrepo, $this->resourcelinkrepo, $this->contextrepo, $this->userrepo ); foreach ($deployments as $deployment) { $deploymentservice->delete_tool_deployment($deployment->get_id()); } } $this->appregistrationrepo->delete($registrationid); } } classes/local/ltiadvantage/service/tool_launch_service.php000064400000043010152266447110020144 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use enrol_lti\helper; use enrol_lti\local\ltiadvantage\entity\context; use enrol_lti\local\ltiadvantage\entity\deployment; use enrol_lti\local\ltiadvantage\entity\migration_claim; use enrol_lti\local\ltiadvantage\entity\resource_link; use enrol_lti\local\ltiadvantage\entity\user; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use Packback\Lti1p3\LtiMessageLaunch; /** * Class tool_launch_service. * * This class handles the launch of a resource by a user, using the LTI Advantage Resource Link Launch. * * See http://www.imsglobal.org/spec/lti/v1p3/#launch-from-a-resource-link * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_launch_service { /** @var deployment_repository $deploymentrepo instance of a deployment repository. */ private $deploymentrepo; /** @var application_registration_repository instance of a application_registration repository */ private $registrationrepo; /** @var resource_link_repository instance of a resource_link repository */ private $resourcelinkrepo; /** @var user_repository instance of a user repository*/ private $userrepo; /** @var context_repository instance of a context repository */ private $contextrepo; /** * The tool_launch_service constructor. * * @param deployment_repository $deploymentrepo instance of a deployment_repository. * @param application_registration_repository $registrationrepo instance of an application_registration_repository. * @param resource_link_repository $resourcelinkrepo instance of a resource_link_repository. * @param user_repository $userrepo instance of a user_repository. * @param context_repository $contextrepo instance of a context_repository. */ public function __construct(deployment_repository $deploymentrepo, application_registration_repository $registrationrepo, resource_link_repository $resourcelinkrepo, user_repository $userrepo, context_repository $contextrepo) { $this->deploymentrepo = $deploymentrepo; $this->registrationrepo = $registrationrepo; $this->resourcelinkrepo = $resourcelinkrepo; $this->userrepo = $userrepo; $this->contextrepo = $contextrepo; } /** Get the launch data from the launch. * * @param LtiMessageLaunch $launch the launch instance. * @return \stdClass the launch data. */ private function get_launch_data(LtiMessageLaunch $launch): \stdClass { $launchdata = $launch->getLaunchData(); $data = [ 'platform' => $launchdata['iss'], // The 'aud' property may be an array with one or more values, but can be a string if there is only one value. // https://www.imsglobal.org/spec/security/v1p1#id-token. 'clientid' => is_array($launchdata['aud']) ? $launchdata['aud'][0] : $launchdata['aud'], 'exp' => $launchdata['exp'], 'nonce' => $launchdata['nonce'], 'sub' => $launchdata['sub'], 'roles' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/roles'], 'deploymentid' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], 'context' => !empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/context']) ? $launchdata['https://purl.imsglobal.org/spec/lti/claim/context'] : null, 'resourcelink' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/resource_link'], 'targetlinkuri' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'], 'custom' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? null, 'launchid' => $launch->getLaunchId(), 'user' => [ 'givenname' => !empty($launchdata['given_name']) ? $launchdata['given_name'] : null, 'familyname' => !empty($launchdata['family_name']) ? $launchdata['family_name'] : null, 'name' => !empty($launchdata['name']) ? $launchdata['name'] : null, 'email' => !empty($launchdata['email']) ? $launchdata['email'] : null, 'picture' => !empty($launchdata['picture']) ? $launchdata['picture'] : null, ], 'ags' => $launchdata['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'] ?? null, 'nrps' => $launchdata['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] ?? null, 'lti1p1' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] ?? null ]; return (object) $data; } /** * Get a context instance from the launch data. * * @param \stdClass $launchdata launch data. * @param deployment $deployment the deployment to which the context belongs. * @return context the context instance. */ private function context_from_launchdata(\stdClass $launchdata, deployment $deployment): context { if ($context = $this->contextrepo->find_by_contextid($launchdata->context['id'], $deployment->get_id())) { // The context has been mapped, just update it. $context->set_types($launchdata->context['type']); } else { // Map a new context. $context = $deployment->add_context($launchdata->context['id'], $launchdata->context['type']); } return $context; } /** * Get a resource_link from the launch data. * * @param \stdClass $launchdata the launch data. * @param \stdClass $resource the resource to which the resource link refers. * @param deployment $deployment the deployment to which the resource_link belongs. * @param context|null $context optional context in which the resource_link lives, null if not needed. * @return resource_link the resource_link instance. */ private function resource_link_from_launchdata(\stdClass $launchdata, \stdClass $resource, deployment $deployment, ?context $context): resource_link { if ($resourcelink = $this->resourcelinkrepo->find_by_deployment($deployment, $launchdata->resourcelink['id'])) { // Resource link exists, so update it. if (isset($context)) { $resourcelink->set_contextid($context->get_id()); } // A resource link may have been updated, via content item selection, to refer to a different resource. if ($resourcelink->get_resourceid() != $resource->id) { $resourcelink->set_resourceid($resource->id); } } else { // Create a new resource link. $resourcelink = $deployment->add_resource_link( $launchdata->resourcelink['id'], $resource->id, $context ? $context->get_id() : null ); } // Add the AGS configuration for the resource link. // See: http://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim. if ($launchdata->ags && (!empty($launchdata->ags['lineitems']) || !empty($launchdata->ags['lineitem']))) { $resourcelink->add_grade_service( !empty($launchdata->ags['lineitems']) ? new \moodle_url($launchdata->ags['lineitems']) : null, !empty($launchdata->ags['lineitem']) ? new \moodle_url($launchdata->ags['lineitem']) : null, $launchdata->ags['scope'] ); } // NRPS. if ($launchdata->nrps) { $resourcelink->add_names_and_roles_service( new \moodle_url($launchdata->nrps['context_memberships_url']), $launchdata->nrps['service_versions'] ); } return $resourcelink; } /** * Get an lti user instance from the launch data. * * @param \stdClass $user the moodle user object. * @param \stdClass $launchdata the launch data. * @param \stdClass $resource the resource to which the user belongs. * @param resource_link $resourcelink the resource_link from which the user originated. * @return user the user instance. */ private function lti_user_from_launchdata(\stdClass $user, \stdClass $launchdata, \stdClass $resource, resource_link $resourcelink): user { // Find the user based on the unique-to-the-issuer 'sub' value. if ($ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) { // User exists, so update existing based on resource data which may have changed. $ltiuser->set_resourcelinkid($resourcelink->get_id()); $ltiuser->set_lang($resource->lang); $ltiuser->set_city($resource->city); $ltiuser->set_country($resource->country); $ltiuser->set_institution($resource->institution); $ltiuser->set_timezone($resource->timezone); $ltiuser->set_maildisplay($resource->maildisplay); } else { // Create the lti user. $ltiuser = $resourcelink->add_user( $user->id, $launchdata->sub, $resource->lang, $resource->city ?? '', $resource->country ?? '', $resource->institution ?? '', $resource->timezone ?? '', $resource->maildisplay ?? null, ); } $ltiuser->set_lastaccess(time()); return $ltiuser; } /** * Get the migration claim from the launch data, or null if not found. * * @param \stdClass $launchdata the launch data. * @return migration_claim|null the claim instance if present in the launch data, else null. */ private function migration_claim_from_launchdata(\stdClass $launchdata): ?migration_claim { if (!isset($launchdata->lti1p1)) { return null; } // Despite the spec requiring the oauth_consumer_key field be present in the migration claim: // (see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key), // Platforms may omit this field making migration impossible. // E.g. for Canvas launches taking place after an assignment_selection placement. if (empty($launchdata->lti1p1['oauth_consumer_key'])) { return null; } return new migration_claim($launchdata->lti1p1, $launchdata->deploymentid, $launchdata->platform, $launchdata->clientid, $launchdata->exp, $launchdata->nonce, new legacy_consumer_repository()); } /** * Check whether the launch user has an admin role. * * @param \stdClass $launchdata the launch data. * @return bool true if the user is admin, false otherwise. */ private function user_is_admin(\stdClass $launchdata): bool { // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. if ($launchdata->roles) { $adminroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator', 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator' ]; foreach ($adminroles as $validrole) { if (in_array($validrole, $launchdata->roles)) { return true; } } } return false; } /** * Check whether the launch user is an instructor. * * @param \stdClass $launchdata the launch data. * @param bool $includelegacy whether to also consider legacy simple names as valid roles. * @return bool true if the user is an instructor, false otherwise. */ private function user_is_staff(\stdClass $launchdata, bool $includelegacy = false): bool { // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. // This method also provides support for (legacy, deprecated) simple names for context roles. // I.e. 'ContentDeveloper' may be supported. if ($launchdata->roles) { $staffroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant' ]; if ($includelegacy) { $staffroles[] = 'ContentDeveloper'; $staffroles[] = 'Instructor'; $staffroles[] = 'Instructor#TeachingAssistant'; } foreach ($staffroles as $validrole) { if (in_array($validrole, $launchdata->roles)) { return true; } } } return false; } /** * Handles the use case "A user launches the tool so they can view an external resource". * * @param \stdClass $user the Moodle user record, obtained via the auth_lti authentication process. * @param LtiMessageLaunch $launch the launch data. * @return array array containing [int $userid, \stdClass $resource] * @throws \moodle_exception if launch problems are encountered. */ public function user_launches_tool(\stdClass $user, LtiMessageLaunch $launch): array { $launchdata = $this->get_launch_data($launch); if (!$registration = $this->registrationrepo->find_by_platform($launchdata->platform, $launchdata->clientid)) { throw new \moodle_exception('ltiadvlauncherror:invalidregistration', 'enrol_lti', '', [$launchdata->platform, $launchdata->clientid]); } if (!$deployment = $this->deploymentrepo->find_by_registration($registration->get_id(), $launchdata->deploymentid)) { throw new \moodle_exception('ltiadvlauncherror:invaliddeployment', 'enrol_lti', '', [$launchdata->deploymentid]); } $resourceuuid = $launchdata->custom['id'] ?? null; if (empty($resourceuuid)) { throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti'); } $resource = array_values(helper::get_lti_tools(['uuid' => $resourceuuid])); $resource = $resource[0] ?? null; if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) { throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid); } // Update the deployment with the legacy consumer_key information, allowing migration of users to take place in future // names and roles syncs. if ($migrationclaim = $this->migration_claim_from_launchdata($launchdata)) { $deployment->set_legacy_consumer_key($migrationclaim->get_consumer_key()); $this->deploymentrepo->save($deployment); } // Save the context, if that claim is present. $context = null; if ($launchdata->context) { $context = $this->context_from_launchdata($launchdata, $deployment); $context = $this->contextrepo->save($context); } // Save the resource link for the tool deployment. $resourcelink = $this->resource_link_from_launchdata($launchdata, $resource, $deployment, $context); $resourcelink = $this->resourcelinkrepo->save($resourcelink); // Save the user launching the resource link. $ltiuser = $this->lti_user_from_launchdata($user, $launchdata, $resource, $resourcelink); $ltiuser = $this->userrepo->save($ltiuser); // Set the frame embedding mode, which controls the display of blocks and nav when launching. global $SESSION; $context = \context::instance_by_id($resource->contextid); $isforceembed = $launchdata->custom['force_embed'] ?? false; $isinstructor = $this->user_is_staff($launchdata, true) || $this->user_is_admin($launchdata); $isforceembed = $isforceembed || ($context->contextlevel == CONTEXT_MODULE && !$isinstructor); if ($isforceembed) { $SESSION->forcepagelayout = 'embedded'; } else { unset($SESSION->forcepagelayout); } // Enrol the user in the course with no role. $result = helper::enrol_user($resource, $ltiuser->get_localid()); if ($result !== helper::ENROLMENT_SUCCESSFUL) { throw new \moodle_exception($result, 'enrol_lti'); } // Give the user the role in the given context. $roleid = $isinstructor ? $resource->roleinstructor : $resource->rolelearner; role_assign($roleid, $ltiuser->get_localid(), $resource->contextid); return [$ltiuser->get_localid(), $resource]; } } classes/local/ltiadvantage/service/tool_deployment_service.php000064400000012757152266447110021070 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use enrol_lti\local\ltiadvantage\entity\deployment; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; /** * Class tool_deployment_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_deployment_service { /** @var application_registration_repository repository to work with application_registration instances. */ private $appregistrationrepo; /** @var deployment_repository repository to work with deployment instances. */ private $deploymentrepo; /** @var resource_link_repository repository to work with resource link instances. */ private $resourcelinkrepo; /** @var context_repository repository to work with context instances. */ private $contextrepo; /** @var user_repository repository to work with user instances. */ private $userrepo; /** * The tool_deployment_service constructor. * * @param application_registration_repository $appregistrationrepo an application_registration_repository instance. * @param deployment_repository $deploymentrepo a deployment_repository instance. * @param resource_link_repository $resourcelinkrepo a resource_link_repository instance. * @param context_repository $contextrepo a context_repository instance. * @param user_repository $userrepo a user_repository instance. */ public function __construct(application_registration_repository $appregistrationrepo, deployment_repository $deploymentrepo, resource_link_repository $resourcelinkrepo, context_repository $contextrepo, user_repository $userrepo) { $this->appregistrationrepo = $appregistrationrepo; $this->deploymentrepo = $deploymentrepo; $this->resourcelinkrepo = $resourcelinkrepo; $this->contextrepo = $contextrepo; $this->userrepo = $userrepo; } /** * Service handling the use case "As an admin I can add a tool deployment to a platform registration". * * @param \stdClass $requestdto the required service data. * @return deployment the deployment instance which has been created. * @throws \coding_exception if the registration doesn't exist. */ public function add_tool_deployment(\stdClass $requestdto): deployment { // DTO contains: registration_id, deployment_id, deployment_name. [ 'registration_id' => $registrationid, 'deployment_id' => $deploymentid, 'deployment_name' => $deploymentname ] = (array) $requestdto; $registration = $this->appregistrationrepo->find($registrationid); if (is_null($registration)) { throw new \coding_exception("Cannot add deployment to non-existent application registration ". "'$registrationid'"); } $deployment = $registration->add_tool_deployment($deploymentname, $deploymentid); return $this->deploymentrepo->save($deployment); } /** * Service handling the use case "As an admin I can delete a tool deployment from a platform registration". * * @param int $deploymentid the id of the deployment to remove. */ public function delete_tool_deployment(int $deploymentid): void { // Delete any resource links attached to this deployment. $this->resourcelinkrepo->delete_by_deployment($deploymentid); // Delete any context entries for the deployment. $this->contextrepo->delete_by_deployment($deploymentid); // Delete all enrolments for any users tied to this deployment. global $DB, $CFG; $sql = "SELECT lu.userid as ltiuserid, e.* FROM {enrol_lti_users} lu JOIN {enrol_lti_tools} lt ON (lt.id = lu.toolid) JOIN {enrol} e ON (e.id = lt.enrolid) WHERE lu.ltideploymentid = :deploymentid"; $instancesrs = $DB->get_recordset_sql($sql, ['deploymentid' => $deploymentid]); require_once($CFG->dirroot . '/enrol/lti/lib.php'); $enrollti = new \enrol_lti_plugin(); foreach ($instancesrs as $instance) { $userid = $instance->ltiuserid; $enrollti->unenrol_user($instance, $userid); } $instancesrs->close(); // Delete any lti user enrolments. $this->userrepo->delete_by_deployment($deploymentid); // Delete the deployment itself. $this->deploymentrepo->delete($deploymentid); } } classes/local/ltiadvantage/repository/application_registration_repository.php000064400000030224152266447110024273 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\application_registration; /** * Class application_registration_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class application_registration_repository { /** @var string $applicationregistrationtable the table containing application registrations. */ private $applicationregistrationtable = 'enrol_lti_app_registration'; /** * Create an application_registration instance from a record. * * @param \stdClass $record the record. * @return application_registration an application_registration instance. */ private function application_registration_from_record(\stdClass $record): application_registration { if ($record->status == application_registration::REGISTRATION_STATUS_INCOMPLETE) { $appreg = application_registration::create_draft( $record->name, $record->uniqueid, $record->id ); if (!empty($record->platformid)) { $appreg->set_platformid(new \moodle_url($record->platformid)); } if (!empty($record->clientid)) { $appreg->set_clientid($record->clientid); } if (!empty($record->authenticationrequesturl)) { $appreg->set_authenticationrequesturl(new \moodle_url($record->authenticationrequesturl)); } if (!empty($record->jwksurl)) { $appreg->set_jwksurl(new \moodle_url($record->jwksurl)); } if (!empty($record->accesstokenurl)) { $appreg->set_accesstokenurl(new \moodle_url($record->accesstokenurl)); } } else if ($record->status == application_registration::REGISTRATION_STATUS_COMPLETE) { $appreg = application_registration::create( $record->name, $record->uniqueid, new \moodle_url($record->platformid), $record->clientid, new \moodle_url($record->authenticationrequesturl), new \moodle_url($record->jwksurl), new \moodle_url($record->accesstokenurl), $record->id ); } return $appreg; } /** * Get an array of application_registration instances from a set of records. * * @param \stdClass[] $records the array of records. * @return array|application_registration[] the array of object instances. */ private function application_registrations_from_records(array $records): array { if (empty($records)) { return []; } return array_map(function($record) { return $this->application_registration_from_record($record); }, $records); } /** * Convert the application_registration object into a stdClass for use with the data store. * * @param application_registration $appregistration the app registration. * @return \stdClass the record. */ private function record_from_application_registration(application_registration $appregistration): \stdClass { $appregistrationrecord = [ 'name' => $appregistration->get_name(), 'uniqueid' => $appregistration->get_uniqueid(), 'status' => $appregistration->is_complete() ? application_registration::REGISTRATION_STATUS_COMPLETE : application_registration::REGISTRATION_STATUS_INCOMPLETE ]; $platformid = $appregistration->get_platformid(); $clientid = $appregistration->get_clientid(); $authrequesturl = $appregistration->get_authenticationrequesturl(); $jwksurl = $appregistration->get_jwksurl(); $accesstokenurl = $appregistration->get_accesstokenurl(); $appregistrationrecord['platformid'] = !is_null($platformid) ? $platformid->out(false) : null; $appregistrationrecord['clientid'] = $clientid; $appregistrationrecord['authenticationrequesturl'] = !is_null($authrequesturl) ? $authrequesturl->out(false) : null; $appregistrationrecord['jwksurl'] = !is_null($jwksurl) ? $jwksurl->out(false) : null; $appregistrationrecord['accesstokenurl'] = !is_null($accesstokenurl) ? $accesstokenurl->out(false) : null; if ($platformid && $clientid) { $indexhash = $this->get_unique_index_hash($appregistration->get_platformid()->out(false), $appregistration->get_clientid()); $appregistrationrecord['platformclienthash'] = $indexhash; } if ($platformid) { $indexhash = $this->get_unique_index_hash($appregistration->get_platformid()->out(false), $appregistration->get_uniqueid()); $appregistrationrecord['platformuniqueidhash'] = $indexhash; } if ($id = $appregistration->get_id()) { $appregistrationrecord['id'] = $id; } return (object) $appregistrationrecord; } /** * Gets a hash of the {platformid, clientid} tuple for use in indexing purposes. * * @param string $platformid the platformid of the registration. * @param string $clientid the clientid of the registration * @return string a SHA256 hash. */ private function get_unique_index_hash(string $platformid, string $clientid): string { return hash('sha256', $platformid . ':' . $clientid); } /** * Find a registration by id. * * @param int $id the id of the application registration. * @return null|application_registration the registration object if found, otherwise null. */ public function find(int $id): ?application_registration { global $DB; try { $record = $DB->get_record($this->applicationregistrationtable, ['id' => $id], '*', MUST_EXIST); return $this->application_registration_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Get all app registrations in the repository. * * @return application_registration[] the array of application registration instances. */ public function find_all(): array { global $DB; return $this->application_registrations_from_records($DB->get_records($this->applicationregistrationtable)); } /** * Find a registration by its unique {platformid, uniqueid} tuple. * * @param string $platformid the url of the platform (the issuer). * @param string $uniqueid the locally uniqueid of the tool registration. * @return application_registration|null application registration instance if found, else null. */ public function find_by_platform_uniqueid(string $platformid, string $uniqueid): ?application_registration { global $DB; try { $indexhash = $this->get_unique_index_hash($platformid, $uniqueid); $record = $DB->get_record($this->applicationregistrationtable, ['platformuniqueidhash' => $indexhash], '*', MUST_EXIST); return $this->application_registration_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Find a registration by its uniqueid. * * @param string $uniqueid the uniqueid identifying the registration. * @return application_registration|null application_registration instance if found, else null. */ public function find_by_uniqueid(string $uniqueid): ?application_registration { global $DB; try { $record = $DB->get_record($this->applicationregistrationtable, ['uniqueid' => $uniqueid], '*', MUST_EXIST); return $this->application_registration_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Find a registration by its unique {platformid, clientid} tuple. * * @param string $platformid the url of the platform (the issuer). * @param string $clientid the client_id of the tool registration on the platform. * @return application_registration|null application registration instance if found, else null. */ public function find_by_platform(string $platformid, string $clientid): ?application_registration { global $DB; try { $indexhash = $this->get_unique_index_hash($platformid, $clientid); $record = $DB->get_record($this->applicationregistrationtable, ['platformclienthash' => $indexhash], '*', MUST_EXIST); return $this->application_registration_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Find an application_registration corresponding to the local id of a given tool deployment. * * @param int $deploymentid the local id of the tool deployment object. * @return application_registration|null the application_registration instance or null if not found. */ public function find_by_deployment(int $deploymentid): ?application_registration { global $DB; try { $sql = "SELECT a.id, a.name, a.platformid, a.clientid, a.authenticationrequesturl, a.jwksurl, a.accesstokenurl, a.uniqueid, a.status, a.timecreated, a.timemodified FROM {enrol_lti_app_registration} a JOIN {enrol_lti_deployment} d ON (d.platformid = a.id) WHERE d.id = :id"; $record = $DB->get_record_sql($sql, ['id' => $deploymentid], MUST_EXIST); return $this->application_registration_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Save an application_registration instance to the store. * * @param application_registration $appregistration the application registration instance. * @return application_registration the saved application registration instance. */ public function save(application_registration $appregistration): application_registration { global $DB; $id = $appregistration->get_id(); $exists = $id ? $this->exists($id) : false; $record = $this->record_from_application_registration($appregistration); $timenow = time(); if ($exists) { $record->timemodified = $timenow; $DB->update_record($this->applicationregistrationtable, $record); } else { $record->timecreated = $record->timemodified = $timenow; $appregid = $DB->insert_record($this->applicationregistrationtable, $record); $record->id = $appregid; } return $this->application_registration_from_record($record); } /** * Report whether an application_registration with id $id exists or not. * * @param int $appregid the id of the application_registration * @return bool true if the object exists, false otherwise. */ public function exists(int $appregid): bool { global $DB; return $DB->record_exists($this->applicationregistrationtable, ['id' => $appregid]); } /** * Delete the application_registration identified by id. * * @param int $id the id of the object to delete. */ public function delete(int $id): void { global $DB; $DB->delete_records($this->applicationregistrationtable, ['id' => $id]); } } classes/local/ltiadvantage/repository/legacy_consumer_repository.php000064400000003770152266447110022363 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; /** * The legacy_consumer_repository class, instances of which are responsible for querying LTI 1.1/2.0 consumer info. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class legacy_consumer_repository { /** * Get a list of all shared secrets which a given LTI 1.1/2.0 consumer is associated with. * * A single consumer key may be used across several tool definitions, with different secrets, thus permitting a * one:many relationship between consumer and secret. * @param string $consumerkey the key identifying the consumer. * @return string[] an array of secrets corresponding to the consumer key. */ public function get_consumer_secrets(string $consumerkey): array { global $DB; $sql = "SELECT t.id, t.secret FROM {enrol_lti_lti2_consumer} c JOIN {enrol_lti_tool_consumer_map} cm ON (c.id = cm.consumerid) JOIN {enrol_lti_tools} t ON (t.id = cm.toolid) WHERE c.consumerkey256 = :consumerkey"; return array_unique(array_column($DB->get_records_sql($sql, ['consumerkey' => $consumerkey]), 'secret')); } } classes/local/ltiadvantage/repository/resource_link_repository.php000064400000026475152266447110022057 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\deployment; use enrol_lti\local\ltiadvantage\entity\resource_link; /** * Class resource_link_repository. * * This class encapsulates persistence logic for \enrol_lti\local\entity\resource_link type objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class resource_link_repository { /** @var string the name of the table to which the entity will be persisted */ private $table = 'enrol_lti_resource_link'; /** @var string the name of the table to which user-entity mappings will have been persisted. */ private $userresourcelinkmaptable = 'enrol_lti_user_resource_link'; /** * Convert a record into an object and return it. * * @param \stdClass $record the record from the store. * @return resource_link a resource_link object. */ private function resource_link_from_record(\stdClass $record): resource_link { $resourcelink = resource_link::create( $record->resourcelinkid, $record->ltideploymentid, $record->resourceid, $record->lticontextid, $record->id ); if ($record->lineitemsservice || $record->lineitemservice) { $scopes = []; if ($record->lineitemscope) { $lineitemscopes = json_decode($record->lineitemscope); foreach ($lineitemscopes as $lineitemscope) { $scopes[] = $lineitemscope; } } if ($record->resultscope) { $scopes[] = $record->resultscope; } if ($record->scorescope) { $scopes[] = $record->scorescope; } $resourcelink->add_grade_service( $record->lineitemsservice ? new \moodle_url($record->lineitemsservice) : null, $record->lineitemservice ? new \moodle_url($record->lineitemservice) : null, $scopes ); } if ($record->contextmembershipsurl) { $resourcelink->add_names_and_roles_service( new \moodle_url($record->contextmembershipsurl), json_decode($record->nrpsserviceversions) ); } return $resourcelink; } /** * Get a list of resource_link objects from a list of records. * * @param array $records the list of records to transform. * @return array the array of resource_link instances. */ private function resource_links_from_records(array $records): array { $resourcelinks = []; foreach ($records as $record) { $resourcelinks[] = $this->resource_link_from_record($record); } return $resourcelinks; } /** * Get a stdClass object ready for persisting, based on the supplied resource_link object. * * @param resource_link $resourcelink the resource link instance. * @return \stdClass the record. */ private function record_from_resource_link(resource_link $resourcelink): \stdClass { $gradeservice = $resourcelink->get_grade_service(); $nrpservice = $resourcelink->get_names_and_roles_service(); $record = [ 'id' => $resourcelink->get_id(), 'resourcelinkid' => $resourcelink->get_resourcelinkid(), 'ltideploymentid' => $resourcelink->get_deploymentid(), 'resourceid' => $resourcelink->get_resourceid(), 'lticontextid' => $resourcelink->get_contextid(), 'lineitemsservice' => null, 'lineitemservice' => null, 'lineitemscope' => null, 'resultscope' => $gradeservice ? $gradeservice->get_resultscope() : null, 'scorescope' => $gradeservice ? $gradeservice->get_scorescope() : null, 'contextmembershipsurl' => $nrpservice ? $nrpservice->get_context_memberships_url()->out(false) : null, 'nrpsserviceversions' => $nrpservice ? json_encode($nrpservice->get_service_versions()) : null ]; if ($gradeservice && ($lineitemsurl = $gradeservice->get_lineitemsurl())) { $record['lineitemsservice'] = $lineitemsurl->out(false); } if ($gradeservice && ($lineitemurl = $gradeservice->get_lineitemurl())) { $record['lineitemservice'] = $lineitemurl->out(false); } if ($gradeservice && ($lineitemscopes = $gradeservice->get_lineitemscope())) { $record['lineitemscope'] = json_encode($lineitemscopes); } return (object) $record; } /** * Save a resource link instance in the store. * * @param resource_link $resourcelink the object to save. * @return resource_link the saved object. */ public function save(resource_link $resourcelink): resource_link { global $DB; $id = $resourcelink->get_id(); $exists = $id ? $this->exists($id) : false; if ($id && !$exists) { throw new \coding_exception("Cannot save resource_link with id '{$id}'. The record does not exist."); } $record = $this->record_from_resource_link($resourcelink); $timenow = time(); if ($exists) { $record->timemodified = $timenow; $DB->update_record($this->table, $record); } else { $record->timecreated = $record->timemodified = $timenow; $id = $DB->insert_record($this->table, $record); $record->id = $id; } return $this->resource_link_from_record($record); } /** * Find and return a resource_link by id. * * @param int $id the id of the resource_link object. * @return resource_link|null the resource_link object, or null if the object cannot be found. */ public function find(int $id): ?resource_link { global $DB; try { $record = $DB->get_record($this->table, ['id' => $id], '*', MUST_EXIST); return $this->resource_link_from_record($record); } catch (\dml_missing_record_exception $ex) { return null; } } /** * Get a resource by id, within a given tool deployment. * * @param deployment $deployment the deployment instance. * @param string $resourcelinkid the resourcelinkid from the platform. * @return resource_link|null the resource link instance, or null if not found. */ public function find_by_deployment(deployment $deployment, string $resourcelinkid): ?resource_link { global $DB; try { $record = $DB->get_record($this->table, ['ltideploymentid' => $deployment->get_id(), 'resourcelinkid' => $resourcelinkid], '*', MUST_EXIST); return $this->resource_link_from_record($record); } catch (\dml_missing_record_exception $ex) { return null; } } /** * Find resource_link objects based on the resource and a given launching user. * * @param int $resourceid the local id of the resource (enrol_lti_tools id) * @param int $userid the local id of the enrol_lti\local\ltiadvantage\user object * @return array an array of resource_links */ public function find_by_resource_and_user(int $resourceid, int $userid): array { global $DB; $sql = "SELECT r.id, r.resourcelinkid, r.resourceid, r.ltideploymentid, r.lticontextid, r.lineitemsservice, r.lineitemservice, r.lineitemscope, r.resultscope, r.scorescope, r.contextmembershipsurl, r.nrpsserviceversions, r.timecreated, r.timemodified FROM {enrol_lti_resource_link} r JOIN {enrol_lti_user_resource_link} ur ON (r.id = ur.resourcelinkid) WHERE ur.ltiuserid = :ltiuserid AND r.resourceid = :resourceid"; $records = $DB->get_records_sql($sql, ['ltiuserid' => $userid, 'resourceid' => $resourceid]); return $this->resource_links_from_records($records); } /** * Gets all mapped resource links for a given resource. * * @param int $resourceid the local id of the shared resource. * @return array the array of resource_link instances. */ public function find_by_resource(int $resourceid): array { global $DB; $records = $DB->get_records($this->table, ['resourceid' => $resourceid]); return $this->resource_links_from_records($records); } /** * Check whether or not the given resource_link object exists. * * @param int $id the unique id the resource_link. * @return bool true if found, false otherwise. */ public function exists(int $id): bool { global $DB; return $DB->record_exists($this->table, ['id' => $id]); } /** * Delete a resource_link based on id. * * @param int $id the id of the resource_link to remove. */ public function delete(int $id) { global $DB; // First remove all enrol_lti_user_resource_link mappings. $DB->delete_records($this->userresourcelinkmaptable, ['resourcelinkid' => $id]); // And the resource_link itself. $DB->delete_records($this->table, ['id' => $id]); } /** * Delete all resource links for a given deployment, as well as any mappings between users and the respective links. * * @param int $deploymentid the id of the deployment instance. */ public function delete_by_deployment(int $deploymentid): void { global $DB; // First remove all enrol_lti_user_resource_link mappings. $DB->delete_records_select( $this->userresourcelinkmaptable, "resourcelinkid IN (SELECT id FROM {{$this->table}} WHERE ltideploymentid = :ltideploymentid)", ['ltideploymentid' => $deploymentid] ); // And remove the resource_link entries themselves. $DB->delete_records($this->table, ['ltideploymentid' => $deploymentid]); } /** * Delete all resource_link instances referring to the resource identified by $resourceid. * * @param int $resourceid the id of the published resource. */ public function delete_by_resource(int $resourceid) { global $DB; // First remove all enrol_lti_user_resource_link mappings. $DB->delete_records_select( $this->userresourcelinkmaptable, "resourcelinkid IN (SELECT id FROM {{$this->table}} WHERE resourceid = :resourceid)", ['resourceid' => $resourceid] ); // And remove the resource_link entries themselves. $DB->delete_records($this->table, ['resourceid' => $resourceid]); } } classes/local/ltiadvantage/repository/user_repository.php000064400000034176152266447110020166 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\user; /** * Class user_repository. * * This class encapsulates persistence logic for \enrol_lti\local\entity\user type objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_repository { /** @var string $ltiuserstable the name of the table to which the entity will be persisted.*/ private $ltiuserstable = 'enrol_lti_users'; /** @var string $userresourcelinkidtable the name of the join table mapping users to resource links.*/ private $userresourcelinkidtable = 'enrol_lti_user_resource_link'; /** * Convert a record into a user object and return it. * * @param \stdClass $userrecord the raw data from relevant tables required to instantiate a user. * @return user a user object. */ private function user_from_record(\stdClass $userrecord): user { return user::create( $userrecord->toolid, $userrecord->localid, $userrecord->ltideploymentid, $userrecord->sourceid, $userrecord->lang, $userrecord->timezone, $userrecord->city, $userrecord->country, $userrecord->institution, $userrecord->maildisplay, $userrecord->lastgrade, $userrecord->lastaccess, $userrecord->resourcelinkid ?? null, (int) $userrecord->id ); } /** * Create a list of user instances from a list of records. * * @param array $records the array of records. * @return array of user instances. */ private function users_from_records(array $records): array { $users = []; foreach ($records as $record) { $users[] = $this->user_from_record($record); } return $users; } /** * Get a stdClass object ready for persisting, based on the supplied user object. * * @param user $user the user instance. * @return \stdClass the record. */ private function user_record_from_user(user $user): \stdClass { return (object) [ 'id' => $user->get_localid(), 'city' => $user->get_city(), 'country' => $user->get_country(), 'institution' => $user->get_institution(), 'timezone' => $user->get_timezone(), 'maildisplay' => $user->get_maildisplay(), 'lang' => $user->get_lang() ]; } /** * Create the corresponding enrol_lti_user record from a user instance. * * @param user $user the user instance. * @return \stdClass the record. */ private function lti_user_record_from_user(user $user): \stdClass { $record = [ 'toolid' => $user->get_resourceid(), 'ltideploymentid' => $user->get_deploymentid(), 'sourceid' => $user->get_sourceid(), 'lastgrade' => $user->get_lastgrade(), 'lastaccess' => $user->get_lastaccess(), ]; if ($user->get_id()) { $record['id'] = $user->get_id(); } return (object) $record; } /** * Helper to validate user:tool uniqueness across a deployment. * * The DB cannot be relied on to do this uniqueness check, since the table is shared by LTI 1.1/2.0 data. * * @param user $user the user instance. * @return bool true if found, false otherwise. */ private function user_exists_for_tool(user $user): bool { // Lack of an id doesn't preclude the object from existence in the store. It may be stale, without an id. // The user can still be found by checking their lti advantage user creds and correlating that to the relevant // lti_user entry (where tool matches the user object's resource). global $DB; $uniquesql = "SELECT lu.id FROM {{$this->ltiuserstable}} lu WHERE lu.toolid = :toolid AND lu.userid = :userid"; $params = ['toolid' => $user->get_resourceid(), 'userid' => $user->get_localid()]; return $DB->record_exists_sql($uniquesql, $params); } /** * Save a user instance in the store. * * @param user $user the object to save. * @return user the saved object. */ public function save(user $user): user { global $DB; $id = $user->get_id(); $exists = !is_null($id) && $this->exists($id); if ($id && !$exists) { throw new \coding_exception("Cannot save lti user with id '{$id}'. The record does not exist."); } $userrecord = $this->user_record_from_user($user); $ltiuserrecord = $this->lti_user_record_from_user($user); $timenow = time(); global $CFG; require_once($CFG->dirroot . '/user/lib.php'); if ($exists) { $ltiuser = $DB->get_record($this->ltiuserstable, ['id' => $ltiuserrecord->id]); $userid = $ltiuser->userid; // Warn about localid vs ltiuser->userid mismatches here. Callers shouldn't be able to force updates using // localid. Only new user associations can be created that way. if (!empty($userrecord->id) && $userid != $userrecord->id) { throw new \coding_exception("Cannot update user mapping. LTI user '{$ltiuser->id}' is already mapped " . "to user '{$ltiuser->userid}' and can't be associated with another user '{$userrecord->id}'."); } // Only update the Moodle user record if something has changed. $rawuser = \core_user::get_user($userrecord->id); $userfieldstocompare = array_intersect_key( (array) $rawuser, (array) $userrecord ); if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) { \user_update_user($userrecord); } unset($userrecord->id); $ltiuserrecord->timemodified = $timenow; $DB->update_record($this->ltiuserstable, $ltiuserrecord); } else { // Validate uniqueness of the lti user, in the case of a stale object coming in to be saved. if ($this->user_exists_for_tool($user)) { throw new \coding_exception("Cannot create duplicate LTI user '{$user->get_localid()}' for resource " . "'{$user->get_resourceid()}'."); } // Only update the Moodle user record if something has changed. $userid = $userrecord->id; $rawuser = \core_user::get_user($userid); $userfieldstocompare = array_intersect_key( (array) $rawuser, (array) $userrecord ); if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) { \user_update_user($userrecord); } unset($userrecord->id); // Create the lti_user record, holding details that have a lifespan equal to that of the enrolment instance. $ltiuserrecord->timecreated = $ltiuserrecord->timemodified = $timenow; $ltiuserrecord->userid = $userid; $ltiuserrecord->id = $DB->insert_record($this->ltiuserstable, $ltiuserrecord); } // If the user was created via a resource_link, create that association. if ($reslinkid = $user->get_resourcelinkid()) { $resourcelinkmap = ['ltiuserid' => $ltiuserrecord->id, 'resourcelinkid' => $reslinkid]; if (!$DB->record_exists($this->userresourcelinkidtable, $resourcelinkmap)) { $DB->insert_record($this->userresourcelinkidtable, $resourcelinkmap); } } $resourcelinkmap = $resourcelinkmap ?? []; // Transform the data into something that looks like a read and can be processed by user_from_record. $record = (object) array_merge( (array) $userrecord, (array) $ltiuserrecord, $resourcelinkmap, ['localid' => $userid] ); return $this->user_from_record($record); } /** * Find and return a user by id. * * @param int $id the id of the user object. * @return user|null the user object, or null if the object cannot be found. */ public function find(int $id): ?user { global $DB; try { $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade, lu.lastaccess, lu.ltideploymentid FROM {{$this->ltiuserstable}} lu JOIN {user} u ON (u.id = lu.userid) WHERE lu.id = :id AND lu.ltideploymentid IS NOT NULL"; $record = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST); return $this->user_from_record($record); } catch (\dml_missing_record_exception $ex) { return null; } } /** * Find an lti user instance by resource. * * @param int $userid the id of the moodle user to look for. * @param int $resourceid the id of the published resource. * @return user|null the lti user instance, or null if not found. */ public function find_single_user_by_resource(int $userid, int $resourceid): ?user { global $DB; try { // Find the lti advantage user record. $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade, lu.lastaccess, lu.ltideploymentid FROM {{$this->ltiuserstable}} lu JOIN {user} u ON (u.id = lu.userid) WHERE lu.userid = :userid AND lu.toolid = :resourceid AND lu.ltideploymentid IS NOT NULL"; $params = ['userid' => $userid, 'resourceid' => $resourceid]; $record = $DB->get_record_sql($sql, $params, MUST_EXIST); return $this->user_from_record($record); } catch (\dml_missing_record_exception $ex) { return null; } } /** * Find all users for a particular shared resource. * * @param int $resourceid the id of the shared resource. * @return array the array of users, empty if none were found. */ public function find_by_resource(int $resourceid): array { global $DB; $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade, lu.lastaccess, lu.ltideploymentid FROM {{$this->ltiuserstable}} lu JOIN {user} u ON (u.id = lu.userid) WHERE lu.toolid = :resourceid AND lu.ltideploymentid IS NOT NULL ORDER BY lu.lastaccess DESC"; $records = $DB->get_records_sql($sql, ['resourceid' => $resourceid]); return $this->users_from_records($records); } /** * Get a list of users associated with the given resource link. * * @param int $resourcelinkid the id of the resource_link instance with which the users are associated. * @return array the array of users, empty if none were found. */ public function find_by_resource_link(int $resourcelinkid) { global $DB; $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade, lu.lastaccess, lu.ltideploymentid FROM {{$this->ltiuserstable}} lu JOIN {user} u ON (u.id = lu.userid) JOIN {{$this->userresourcelinkidtable}} url ON (url.ltiuserid = lu.id) WHERE url.resourcelinkid = :resourcelinkid ORDER BY lu.lastaccess DESC"; $records = $DB->get_records_sql($sql, ['resourcelinkid' => $resourcelinkid]); return $this->users_from_records($records); } /** * Check whether or not the given user object exists. * * @param int $id the unique id the user. * @return bool true if found, false otherwise. */ public function exists(int $id): bool { global $DB; return $DB->record_exists($this->ltiuserstable, ['id' => $id]); } /** * Delete a user based on id. * * @param int $id the id of the user to remove. */ public function delete(int $id) { global $DB; $DB->delete_records($this->ltiuserstable, ['id' => $id]); $DB->delete_records($this->userresourcelinkidtable, ['ltiuserid' => $id]); } /** * Delete all lti user instances based on a given local deployment instance id. * * @param int $deploymentid the local id of the deployment instance to which the users belong. */ public function delete_by_deployment(int $deploymentid): void { global $DB; $DB->delete_records($this->ltiuserstable, ['ltideploymentid' => $deploymentid]); } } classes/local/ltiadvantage/repository/deployment_repository.php000064400000016571152266447110021367 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\deployment; /** * The deployment_repository class. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class deployment_repository { /** @var string $deploymenttable the name of table containing deployments. */ private $deploymenttable = 'enrol_lti_deployment'; /** * Create a valid record from a deployment instance. * * @param deployment $deployment the deployment. * @return \stdClass a compatible record. */ private function record_from_deployment(deployment $deployment): \stdClass { $record = (object) [ 'name' => $deployment->get_deploymentname(), 'deploymentid' => $deployment->get_deploymentid(), 'platformid' => $deployment->get_registrationid(), 'legacyconsumerkey' => $deployment->get_legacy_consumer_key() ]; if ($id = $deployment->get_id()) { $record->id = $id; } return $record; } /** * Create a list of deployments based on a list of records. * * @param array $records an array of deployment records. * @return deployment[] */ private function deployments_from_records(array $records): array { if (empty($records)) { return []; } return array_map(function($record) { return $this->deployment_from_record($record); }, $records); } /** * Create a valid deployment from a record. * * @param \stdClass $record the record. * @return deployment the deployment instance. */ private function deployment_from_record(\stdClass $record): deployment { $deployment = deployment::create( $record->platformid, $record->deploymentid, $record->name, $record->id, $record->legacyconsumerkey ); return $deployment; } /** * Save a deployment to the store. * * @param deployment $deployment the deployment instance to save. * @return deployment the saved deployment instance. */ public function save(deployment $deployment): deployment { global $DB; $id = $deployment->get_id(); $exists = $id ? $this->exists($id) : false; $record = $this->record_from_deployment($deployment); $timenow = time(); if ($exists) { $record->timemodified = $timenow; $DB->update_record($this->deploymenttable, $record); } else { $record->timecreated = $record->timemodified = $timenow; $id = $DB->insert_record($this->deploymenttable, $record); $record->id = $id; } return $this->deployment_from_record($record); } /** * Find and return a deployment, by id. * * @param int $id the id of the deployment to find. * @return deployment|null */ public function find(int $id): ?deployment { global $DB; try { $record = $DB->get_record($this->deploymenttable, ['id' => $id], '*', MUST_EXIST); return $this->deployment_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Determine whether a deployment exists in the repository. * * @param int $id the identifier of the deployment * @return bool true if the deployment exists, false otherwise. */ public function exists(int $id): bool { global $DB; return $DB->record_exists($this->deploymenttable, ['id' => $id]); } /** * Delete a deployment from the store. * * @param int $id the id of the deployment object to remove. */ public function delete(int $id): void { global $DB; $DB->delete_records($this->deploymenttable, ['id' => $id]); } /** * Delete all deployments for the given registration. * * @param int $registrationid the registration id. */ public function delete_by_registration(int $registrationid): void { global $DB; $DB->delete_records($this->deploymenttable, ['platformid' => $registrationid]); } /** * Return a count of how many deployments exists for a given application_registration. * * @param int $registrationid the id of the application_registration instance. * @return int the number of deployments found. */ public function count_by_registration(int $registrationid): int { global $DB; return $DB->count_records($this->deploymenttable, ['platformid' => $registrationid]); } /** * Get a deployment based on its deploymentid and a for a given application registration id. * * @param int $registrationid the id of the application_registration to which the deployment belongs. * @param string $deploymentid the deploymentid of the deployment, as set by the platform. * @return deployment|null deployment if found, otherwise null. */ public function find_by_registration(int $registrationid, string $deploymentid): ?deployment { global $DB; try { $sql = "SELECT eld.id, eld.name, eld.deploymentid, eld.platformid, eld.legacyconsumerkey FROM {".$this->deploymenttable."} eld JOIN {enrol_lti_app_registration} elar ON (eld.platformid = elar.id) WHERE elar.id = :registrationid AND eld.deploymentid = :deploymentid"; $params = ['registrationid' => $registrationid, 'deploymentid' => $deploymentid]; $record = $DB->get_record_sql($sql, $params, MUST_EXIST); return $this->deployment_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Get all deployments for a given application registration id. * * @param int $registrationid the id of the application_registration to which the deployment belongs. * @return deployment[]|null deployments if found, otherwise null. */ public function find_all_by_registration(int $registrationid): ?array { global $DB; $sql = "SELECT eld.id, eld.name, eld.deploymentid, eld.platformid, eld.legacyconsumerkey FROM {".$this->deploymenttable."} eld JOIN {enrol_lti_app_registration} elar ON (eld.platformid = elar.id) WHERE elar.id = :registrationid"; $records = $DB->get_records_sql($sql, ['registrationid' => $registrationid]); return $this->deployments_from_records($records); } } classes/local/ltiadvantage/repository/context_repository.php000064400000012025152266447110020661 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\context; /** * Class context_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_repository { /** @var string the name of the table storing object data. */ private $contexttable = 'enrol_lti_context'; /** * Generate a context instance from a record. * * @param \stdClass $record the record. * @return context the context instance. */ private function context_from_record(\stdClass $record): context { $context = context::create( $record->ltideploymentid, $record->contextid, json_decode($record->type), $record->id ); return $context; } /** * Generate a record from a context instance. * * @param context $context the context instance. * @return \stdClass the resulting record. */ private function record_from_context(context $context): \stdClass { $record = [ 'contextid' => $context->get_contextid(), 'ltideploymentid' => $context->get_deploymentid(), 'type' => json_encode($context->get_types()), ]; if ($id = $context->get_id()) { $record['id'] = $id; } return (object) $record; } /** * Save the context to the store. * * @param context $context the context to save. * @return context the saved context instance. */ public function save(context $context): context { global $DB; $id = $context->get_id(); $exists = $id ? $this->exists($id) : false; $record = $this->record_from_context($context); $timenow = time(); if ($exists) { $record->timemodified = $timenow; $DB->update_record($this->contexttable, $record); } else { $record->timecreated = $record->timemodified = $timenow; $id = $DB->insert_record($this->contexttable, $record); $record->id = $id; } return $this->context_from_record($record); } /** * Find a context by id. * * @param int $id the id of the instance. * @return context|null the context, if found, else null. */ public function find(int $id): ?context { global $DB; try { $record = $DB->get_record($this->contexttable, ['id' => $id], '*', MUST_EXIST); return $this->context_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Find a context by it's platform-issued context id string. * * @param string $contextid the id of the context on the platform. * @param int $deploymentid the id of the local deployment instance in which the contextid is unique. * @return context|null the context instance, if found, else null. */ public function find_by_contextid(string $contextid, int $deploymentid): ?context { global $DB; try { $record = $DB->get_record($this->contexttable, ['contextid' => $contextid, 'ltideploymentid' => $deploymentid], '*', MUST_EXIST); return $this->context_from_record($record); } catch (\dml_missing_record_exception $e) { return null; } } /** * Check whether the context identified by 'id' exists in the store. * * @param int $id the id of the instance to check. * @return bool true if found, false otherwise. */ public function exists(int $id): bool { global $DB; return $DB->record_exists($this->contexttable, ['id' => $id]); } /** * Delete the context identified by 'id' from the store. * * @param int $id the id of context to delete. */ public function delete(int $id): void { global $DB; $DB->delete_records($this->contexttable, ['id' => $id]); } /** * Delete all contexts under a given deployment. * * @param int $deploymentid the id of the local deployment instance. */ public function delete_by_deployment(int $deploymentid): void { global $DB; $DB->delete_records($this->contexttable, ['ltideploymentid' => $deploymentid]); } } classes/local/ltiadvantage/repository/published_resource_repository.php000064400000023606152266447110023072 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use core_availability\info_module; use enrol_lti\local\ltiadvantage\viewobject\published_resource; /** * Class published_resource_repository for fetching the published_resource instances from the store. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class published_resource_repository { /** * Convert a list of stdClasses to a list of published_resource instances. * * @param array $records the records. * @return array the array of published_resource instances. */ private function published_resources_from_records(array $records): array { $publishedresources = []; foreach ($records as $record) { $publishedresource = new published_resource( $record->name, $record->coursefullname, $record->courseid, $record->contextid, $record->id, $record->uuid, $record->supportsgrades, $record->grademax ?? null, $record->iscourse, ); $publishedresources[] = $publishedresource; } return $publishedresources; } /** * Given a list of published resources, return a list of those which are available to the provided user. * * @param array $resources the array of records representing published resources. * @param int $userid the Moodle user id to check. * @return array an array of stdClasses containing data about resources which are available to the current user. */ private function get_available_resources_from_records(array $resources, int $userid): array { global $CFG; require_once($CFG->libdir . '/gradelib.php'); require_once($CFG->libdir . '/moodlelib.php'); $availableresources = []; foreach ($resources as $resource) { if ($resource->contextlevel == CONTEXT_COURSE) { // Shared item is a course. if (!can_access_course(get_course($resource->courseid), $userid)) { continue; } $resource->name = format_string($resource->coursefullname, true, ['context' => $resource->contextid]); $resource->coursefullname = $resource->name; $resource->iscourse = true; $resource->supportsgrades = true; $coursegradeitem = \grade_item::fetch_course_item($resource->courseid); $resource->grademax = $coursegradeitem->grademax; $availableresources[] = $resource; } else if ($resource->contextlevel = CONTEXT_MODULE) { // Shared item is a module. $resource->coursefullname = format_string($resource->coursefullname, true, ['context' => $resource->contextid]); $mods = get_fast_modinfo($resource->courseid, $userid)->get_cms(); foreach ($mods as $mod) { if ($mod->context->id == $resource->contextid) { if (info_module::is_user_visible($mod->id, $userid, true)) { $resource->iscourse = false; $resource->name = $mod->name; $resource->supportsgrades = false; $resource->grademax = null; // Only activities having a single grade item of GRADE_TYPE_VALUE are eligible for declarative binding. if (plugin_supports('mod', $mod->modname, FEATURE_GRADE_HAS_GRADE)) { $gradinginfo = grade_get_grades($resource->courseid, 'mod', $mod->modname, $mod->instance); if (count($gradinginfo->items) == 1) { $gradeitem = \grade_item::fetch([ 'courseid' => $resource->courseid, 'itemtype' => 'mod', 'itemmodule' => $mod->modname, 'iteminstance' => $mod->instance ]); if ($gradeitem && $gradeitem->gradetype == GRADE_TYPE_VALUE) { $resource->supportsgrades = true; $resource->grademax = (int)$gradinginfo->items[0]->grademax; } } } $availableresources[] = $resource; } } } } } return $availableresources; } /** * Find all published resources which are visible to the given user. * * @param int $userid the id of the user to check. * @return published_resource[] an array of published_resource view objects instances. */ public function find_all_for_user(int $userid): array { global $DB, $CFG; require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/enrollib.php'); require_once($CFG->libdir . '/moodlelib.php'); require_once($CFG->libdir . '/modinfolib.php'); require_once($CFG->libdir . '/weblib.php'); [$insql, $inparams] = $DB->get_in_or_equal(['LTI-1p3'], SQL_PARAMS_NAMED); $sql = "SELECT elt.id, elt.uuid, elt.enrolid, elt.contextid, elt.institution, elt.lang, elt.timezone, elt.maxenrolled, elt.maildisplay, elt.city, elt.country, elt.gradesync, elt.gradesynccompletion, elt.membersync, elt.membersyncmode, elt.roleinstructor, elt.rolelearner, e.name AS enrolname, e.courseid, ctx.contextlevel, c.fullname AS coursefullname FROM {enrol} e JOIN {enrol_lti_tools} elt ON (e.id = elt.enrolid and e.status = :enrolstatusenabled) JOIN {course} c ON (c.id = e.courseid) JOIN {context} ctx ON (ctx.id = elt.contextid) WHERE elt.ltiversion $insql ORDER BY courseid"; $params = array_merge($inparams, ['enrolstatusenabled' => ENROL_INSTANCE_ENABLED]); $resources = $DB->get_records_sql($sql, $params); // Only users who have the ability to publish content should see published content. $resources = array_filter($resources, function($resource) use ($userid) { return has_capability('enrol/lti:config', \context_course::instance($resource->courseid), $userid); }); // Make sure the user can access each course or module, excluding those which are inaccessible from the return. $availableresources = $this->get_available_resources_from_records($resources, $userid); return $this->published_resources_from_records($availableresources); } /** * Find all published_resource instances matching the supplied ids for the current user. * * @param array $ids the array containing object ids to lookup * @param int $userid the id of the user to check * @return array an array of published_resource instances which are available to the user. */ public function find_all_by_ids_for_user(array $ids, int $userid): array { global $DB, $CFG; if (empty($ids)) { return []; } require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/enrollib.php'); require_once($CFG->libdir . '/moodlelib.php'); require_once($CFG->libdir . '/modinfolib.php'); require_once($CFG->libdir . '/weblib.php'); [$insql, $inparams] = $DB->get_in_or_equal(['LTI-1p3'], SQL_PARAMS_NAMED); [$idsinsql, $idsinparams] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $sql = "SELECT elt.id, elt.uuid, elt.enrolid, elt.contextid, elt.institution, elt.lang, elt.timezone, elt.maxenrolled, elt.maildisplay, elt.city, elt.country, elt.gradesync, elt.gradesynccompletion, elt.membersync, elt.membersyncmode, elt.roleinstructor, elt.rolelearner, e.name AS enrolname, e.courseid, ctx.contextlevel, c.fullname AS coursefullname FROM {enrol} e JOIN {enrol_lti_tools} elt ON (e.id = elt.enrolid and e.status = :enrolstatusenabled) JOIN {course} c ON (c.id = e.courseid) JOIN {context} ctx ON (ctx.id = elt.contextid) WHERE elt.ltiversion $insql AND elt.id $idsinsql ORDER BY courseid"; $params = array_merge($inparams, $idsinparams, ['enrolstatusenabled' => ENROL_INSTANCE_ENABLED]); $resources = $DB->get_records_sql($sql, $params); // Make sure the user can access each course or module, excluding those which are inaccessible from the return. $availableresources = $this->get_available_resources_from_records($resources, $userid); return $this->published_resources_from_records($availableresources); } } classes/local/ltiadvantage/admin/admin_setting_registeredplatforms.php000064400000006160152266447110022544 0ustar00. namespace enrol_lti\local\ltiadvantage\admin; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; /** * The admin_setting_registeredplatforms class, for rendering a table of platforms which have been registered. * * This setting is useful for LTI 1.3 only. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class admin_setting_registeredplatforms extends \admin_setting { /** * Calls parent::__construct with specific arguments */ public function __construct() { $this->nosave = true; parent::__construct('enrol_lti_tool_registered_platforms', get_string('registeredplatforms', 'enrol_lti'), '', ''); } /** * Always returns true, does nothing. * * @return bool true. */ public function get_setting() { return true; } /** * Always returns true, does nothing. * * @return bool true. */ public function get_defaultsetting() { return true; } /** * Always returns '', does not write anything. * * @param string|array $data the data * @return string Always returns ''. */ public function write_setting($data) { return ''; } /** * Checks if $query is one of the available external services * * @param string $query The string to search for * @return bool Returns true if found, false if not */ public function is_related($query) { if (parent::is_related($query)) { return true; } $appregistrationrepo = new application_registration_repository(); $registrations = $appregistrationrepo->find_all(); foreach ($registrations as $reg) { if (stripos($reg->get_name(), $query) !== false) { return true; } } return false; } /** * Builds the HTML to display the table. * * @param string $data Unused * @param string $query * @return string */ public function output_html($data, $query='') { global $PAGE; $appregistrationrepo = new application_registration_repository(); $renderer = $PAGE->get_renderer('enrol_lti'); $return = $renderer->render_admin_setting_registered_platforms($appregistrationrepo->find_all()); return highlight($query, $return); } } classes/local/ltiadvantage/task/sync_grades.php000064400000004724152266447110015731 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use core\task\scheduled_task; use enrol_lti\helper; /** * LTI Advantage task responsible for pushing grades to tool platforms. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_grades extends scheduled_task { /** * Get a descriptive name for this task. * * @return string */ public function get_name() { return get_string('tasksyncgrades', 'enrol_lti'); } /** * Creates adhoc tasks (one per resource) to synchronize grades from the tool to any registered platforms. * * @return bool|void */ public function execute() { if (!is_enabled_auth('lti')) { mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti'))); return true; } if (!enrol_is_enabled('lti')) { mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti')); return true; } $resources = helper::get_lti_tools([ 'status' => ENROL_INSTANCE_ENABLED, 'gradesync' => 1, 'ltiversion' => 'LTI-1p3' ]); if (empty($resources)) { mtrace('Skipping task - There are no resources with grade sync enabled.'); return true; } foreach ($resources as $resource) { $task = new \enrol_lti\local\ltiadvantage\task\sync_tool_grades(); $task->set_custom_data($resource); $task->set_component('enrol_lti'); \core\task\manager::queue_adhoc_task($task, true); } mtrace('Spawned ' . count($resources) . ' adhoc tasks to sync grades.'); } } classes/local/ltiadvantage/task/sync_tool_grades.php000064400000032706152266447110016767 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use core\http_client; use core\task\adhoc_task; use enrol_lti\local\ltiadvantage\lib\issuer_database; use enrol_lti\local\ltiadvantage\lib\launch_cache_session; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use Packback\Lti1p3\LtiAssignmentsGradesService; use Packback\Lti1p3\LtiGrade; use Packback\Lti1p3\LtiLineitem; use Packback\Lti1p3\LtiRegistration; use Packback\Lti1p3\LtiServiceConnector; /** * LTI Advantage task responsible for pushing grades to tool platforms. * * @package enrol_lti * @copyright 2023 David Pesce * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_tool_grades extends adhoc_task { /** * Sync grades to the platform using the Assignment and Grade Services (AGS). * * @param \stdClass $resource the enrol_lti_tools data record for the shared resource. * @return array an array containing the */ protected function sync_grades_for_resource($resource): array { $usercount = 0; $sendcount = 0; $userrepo = new user_repository(); $resourcelinkrepo = new resource_link_repository(); $appregistrationrepo = new application_registration_repository(); $issuerdb = new issuer_database($appregistrationrepo, new deployment_repository()); if ($users = $userrepo->find_by_resource($resource->id)) { $completion = new \completion_info(get_course($resource->courseid)); $syncedusergrades = []; // Keep track of those users who have had their grade synced during this run. foreach ($users as $user) { $mtracecontent = "for the user '{$user->get_localid()}', for the resource '$resource->id' and the course " . "'$resource->courseid'"; $usercount++; // Check if we do not have a grade service endpoint in either of the resource links. // Remember, not all launches need to support grade services. $userresourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id()); $userlastgrade = $user->get_lastgrade(); mtrace("Found ".count($userresourcelinks)." resource link(s) $mtracecontent. Attempting to sync grades for all."); foreach ($userresourcelinks as $userresourcelink) { mtrace("Processing resource link '{$userresourcelink->get_resourcelinkid()}'."); if (!$gradeservice = $userresourcelink->get_grade_service()) { mtrace("Skipping - No grade service found $mtracecontent."); continue; } if (!$context = \context::instance_by_id($resource->contextid, IGNORE_MISSING)) { mtrace("Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'."); continue; } $grade = false; $dategraded = false; if ($context->contextlevel == CONTEXT_COURSE) { if ($resource->gradesynccompletion && !$completion->is_course_complete($user->get_localid())) { mtrace("Skipping - Course not completed $mtracecontent."); continue; } // Get the grade. if ($grade = grade_get_course_grade($user->get_localid(), $resource->courseid)) { $grademax = floatval($grade->item->grademax); $dategraded = $grade->dategraded; $grade = $grade->grade; } } else if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); if ($resource->gradesynccompletion) { $data = $completion->get_data($cm, false, $user->get_localid()); if (!in_array($data->completionstate, [COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE])) { mtrace("Skipping - Activity not completed $mtracecontent."); continue; } } $grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, $user->get_localid()); if (!empty($grades->items[0]->grades)) { $grade = reset($grades->items[0]->grades); if (!empty($grade->item)) { $grademax = floatval($grade->item->grademax); } else { $grademax = floatval($grades->items[0]->grademax); } $dategraded = $grade->dategraded; $grade = $grade->grade; } } if ($grade === false || $grade === null || strlen($grade) < 1) { mtrace("Skipping - Invalid grade $mtracecontent."); continue; } $grade = floatval($grade); // Grade must be sent as a numeric value, not a string. if (empty($grademax)) { mtrace("Skipping - Invalid grademax $mtracecontent."); continue; } if (!grade_floats_different($grade, $userlastgrade)) { mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same."); continue; } $floatgrade = $grade / $grademax; try { // Get an AGS instance for the corresponding application registration and service data. $appregistration = $appregistrationrepo->find_by_deployment( $userresourcelink->get_deploymentid() ); $registration = $issuerdb->findRegistrationByIssuer( $appregistration->get_platformid()->out(false), $appregistration->get_clientid() ); global $CFG; require_once($CFG->libdir . '/filelib.php'); $sc = new LtiServiceConnector(new launch_cache_session(), new http_client()); $lineitemurl = $gradeservice->get_lineitemurl(); $lineitemsurl = $gradeservice->get_lineitemsurl(); $servicedata = [ 'lineitems' => $lineitemsurl ? $lineitemsurl->out(false) : null, 'lineitem' => $lineitemurl ? $lineitemurl->out(false) : null, 'scope' => $gradeservice->get_scopes(), ]; $ags = $this->get_ags($sc, $registration, $servicedata); $ltigrade = LtiGrade::new() ->setScoreGiven($grade) ->setScoreMaximum($grademax) ->setUserId($user->get_sourceid()) ->setTimestamp(date(\DateTimeInterface::ISO8601, $dategraded)) ->setActivityProgress('Completed') ->setGradingProgress('FullyGraded'); if (empty($servicedata['lineitem'])) { // The launch did not include a couple lineitem, so find or create the line item for grading. $lineitem = $ags->findOrCreateLineitem(new LtiLineitem([ 'label' => $this->get_line_item_label($resource, $context), 'scoreMaximum' => $grademax, 'tag' => 'grade', 'resourceId' => $userresourcelink->get_resourceid(), 'resourceLinkId' => $userresourcelink->get_resourcelinkid() ])); $response = $ags->putGrade($ltigrade, $lineitem); } else { // Let AGS find the coupled line item. $response = $ags->putGrade($ltigrade); } } catch (\Exception $e) { mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); mtrace($e->getMessage()); continue; } $successresponses = [200, 201, 202, 204]; if (in_array($response['status'], $successresponses)) { $user->set_lastgrade(grade_floatval($grade)); $syncedusergrades[$user->get_id()] = $user; mtrace("Success - The grade '$floatgrade' $mtracecontent was sent."); } else { mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); mtrace("Header: {$response['headers']['httpstatus']}"); } } } // Update the lastgrade value for any users who had a grade synced. Allows skipping on future runs if not changed. // Update the count of total users having their grades synced, not the total number of grade sync calls made. foreach ($syncedusergrades as $ltiuser) { $userrepo->save($ltiuser); $sendcount = $sendcount + 1; } } return [$usercount, $sendcount]; } /** * Get the string label for the line item associated with the resource, based on the course or module name. * * @param \stdClass $resource the enrol_lti_tools record. * @param \context $context the context of the resource - either course or module. * @return string the label to use in the line item. */ protected function get_line_item_label(\stdClass $resource, \context $context): string { $resourcename = 'default'; if ($context->contextlevel == CONTEXT_COURSE) { global $DB; $coursenamesql = "SELECT c.fullname FROM {enrol_lti_tools} t JOIN {enrol} e ON (e.id = t.enrolid) JOIN {course} c ON (c.id = e.courseid) WHERE t.id = :resourceid"; $coursename = $DB->get_field_sql($coursenamesql, ['resourceid' => $resource->id]); $resourcename = format_string($coursename, true, ['context' => $context->id]); } else if ($context->contextlevel == CONTEXT_MODULE) { foreach (get_fast_modinfo($resource->courseid)->get_cms() as $mod) { if ($mod->context->id == $context->id) { $resourcename = $mod->name; } } } return $resourcename; } /** * Get an Assignment and Grade Services (AGS) instance to make the call to the platform. * * @param LtiServiceConnector $sc a service connector instance. * @param LtiRegistration $registration the registration instance. * @param array $sd the service data. * @return LtiAssignmentsGradesService */ protected function get_ags(LtiServiceConnector $sc, LtiRegistration $registration, array $sd): LtiAssignmentsGradesService { return new LtiAssignmentsGradesService($sc, $registration, $sd); } /** * Performs the synchronisation of grades from the tool to any registered platforms. * * @return bool|void */ public function execute() { global $CFG; require_once($CFG->dirroot . '/lib/completionlib.php'); require_once($CFG->libdir . '/gradelib.php'); require_once($CFG->dirroot . '/grade/querylib.php'); $resource = $this->get_custom_data(); mtrace("Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$resource->courseid'."); [$usercount, $sendcount] = $this->sync_grades_for_resource($resource); mtrace("Completed - Synced grades for tool '$resource->id' in the course '$resource->courseid'. " . "Processed $usercount users; sent $sendcount grades."); mtrace(""); } } classes/local/ltiadvantage/task/sync_members.php000064400000050263152266447110016115 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use core\http_client; use core\task\scheduled_task; use enrol_lti\helper; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\entity\nrps_info; use enrol_lti\local\ltiadvantage\entity\resource_link; use enrol_lti\local\ltiadvantage\entity\user; use enrol_lti\local\ltiadvantage\lib\issuer_database; use enrol_lti\local\ltiadvantage\lib\launch_cache_session; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use Packback\Lti1p3\LtiNamesRolesProvisioningService; use Packback\Lti1p3\LtiRegistration; use Packback\Lti1p3\LtiServiceConnector; use stdClass; /** * LTI Advantage-specific task responsible for syncing memberships from tool platforms with the tool. * * This task may gather members from a context-level service call, depending on whether a resource-level service call * (which is made first) was successful. Because of the context-wide memberships, and because each published resource * has per-resource access control (role assignments), this task only enrols user into the course, and does not assign * roles to resource/course contexts. Role assignment only takes place during a launch, via the tool_launch_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_members extends scheduled_task { /** @var array Array of user photos. */ protected $userphotos = []; /** @var resource_link_repository $resourcelinkrepo for fetching resource_link instances.*/ protected $resourcelinkrepo; /** @var application_registration_repository $appregistrationrepo for fetching application_registration instances.*/ protected $appregistrationrepo; /** @var deployment_repository $deploymentrepo for fetching deployment instances. */ protected $deploymentrepo; /** @var user_repository $userrepo for fetching and saving lti user information.*/ protected $userrepo; /** @var issuer_database $issuerdb library specific registration DB required to create service connectors.*/ protected $issuerdb; /** * Get the name for this task. * * @return string the name of the task. */ public function get_name(): string { return get_string('tasksyncmembers', 'enrol_lti'); } /** * Make a resource-link-level memberships call. * * @param nrps_info $nrps information about names and roles service endpoints and scopes. * @param LtiServiceConnector $sc a service connector object. * @param LtiRegistration $registration the registration * @param resource_link $resourcelink the resource link * @return array an array of members if found. */ protected function get_resource_link_level_members(nrps_info $nrps, LtiServiceConnector $sc, LtiRegistration $registration, resource_link $resourcelink) { // Try a resource-link-level memberships call first, falling back to context-level if no members are found. $reslinkmembershipsurl = $nrps->get_context_memberships_url(); $reslinkmembershipsurl->param('rlid', $resourcelink->get_resourcelinkid()); $servicedata = [ 'context_memberships_url' => $reslinkmembershipsurl->out(false) ]; $reslinklevelnrps = new LtiNamesRolesProvisioningService($sc, $registration, $servicedata); mtrace('Making resource-link-level memberships request'); return $reslinklevelnrps->getMembers(); } /** * Make a context-level memberships call. * * @param nrps_info $nrps information about names and roles service endpoints and scopes. * @param LtiServiceConnector $sc a service connector object. * @param LtiRegistration $registration the registration * @return array an array of members. */ protected function get_context_level_members(nrps_info $nrps, LtiServiceConnector $sc, LtiRegistration $registration) { $clservicedata = [ 'context_memberships_url' => $nrps->get_context_memberships_url()->out(false) ]; $contextlevelnrps = new LtiNamesRolesProvisioningService($sc, $registration, $clservicedata); return $contextlevelnrps->getMembers(); } /** * Make the NRPS service call and fetch members based on the given resource link. * * Memberships will be retrieved by first trying the link-level memberships service first, falling back to calling * the context-level memberships service only if the link-level call fails. * * @param application_registration $appregistration an application registration instance. * @param resource_link $resourcelink a resourcelink instance. * @return array an array of members. */ protected function get_members_from_resource_link(application_registration $appregistration, resource_link $resourcelink) { // Get a service worker for the corresponding application registration. $registration = $this->issuerdb->findRegistrationByIssuer( $appregistration->get_platformid()->out(false), $appregistration->get_clientid() ); global $CFG; require_once($CFG->libdir . '/filelib.php'); $sc = new LtiServiceConnector(new launch_cache_session(), new http_client()); $nrps = $resourcelink->get_names_and_roles_service(); try { $members = $this->get_resource_link_level_members($nrps, $sc, $registration, $resourcelink); } catch (\Exception $e) { mtrace('Link-level memberships request failed. Making context-level memberships request'); $members = $this->get_context_level_members($nrps, $sc, $registration); } return $members; } /** * Performs the synchronisation of members. */ public function execute() { if (!is_enabled_auth('lti')) { mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti'))); return; } if (!enrol_is_enabled('lti')) { mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti')); return; } $this->resourcelinkrepo = new resource_link_repository(); $this->appregistrationrepo = new application_registration_repository(); $this->deploymentrepo = new deployment_repository(); $this->userrepo = new user_repository(); $this->issuerdb = new issuer_database($this->appregistrationrepo, $this->deploymentrepo); $resources = helper::get_lti_tools(['status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1, 'ltiversion' => 'LTI-1p3']); foreach ($resources as $resource) { mtrace("Starting - Member sync for published resource '$resource->id' for course '$resource->courseid'."); $usercount = 0; $enrolcount = 0; $unenrolcount = 0; $syncedusers = []; // Get all resource_links for this shared resource. // This is how context/resource_link memberships calls will be made. $resourcelinks = $this->resourcelinkrepo->find_by_resource((int)$resource->id); foreach ($resourcelinks as $resourcelink) { mtrace("Requesting names and roles for the resource link '{$resourcelink->get_id()}' for the resource" . " '{$resource->id}'"); if (!$resourcelink->get_names_and_roles_service()) { mtrace("Skipping - No names and roles service found."); continue; } $appregistration = $this->appregistrationrepo->find_by_deployment( $resourcelink->get_deploymentid() ); if (!$appregistration) { mtrace("Skipping - no corresponding application registration found."); continue; } try { $members = $this->get_members_from_resource_link($appregistration, $resourcelink); } catch (\Exception $e) { mtrace("Skipping - Names and Roles service request failed: {$e->getMessage()}."); continue; } // Fetched members count. $membercount = count($members); $usercount += $membercount; mtrace("$membercount members received."); // Process member information. [$rlenrolcount, $userids] = $this->sync_member_information($appregistration, $resource, $resourcelink, $members); $enrolcount += $rlenrolcount; // Update the list of users synced for this shared resource or its context. $syncedusers = array_unique(array_merge($syncedusers, $userids)); mtrace("Completed - Synced $membercount members for the resource link '{$resourcelink->get_id()}' ". "for the resource '{$resource->id}'.\n"); // Sync unenrolments on a per-resource-link basis so we have fine grained control over unenrolments. // If a resource link doesn't support NRPS, it will already have been skipped. $unenrolcount += $this->sync_unenrol_resourcelink($resourcelink, $resource, $syncedusers); } mtrace("Completed - Synced members for tool '$resource->id' in the course '$resource->courseid'. " . "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n"); } if (!empty($resources) && !empty($this->userphotos)) { // Sync the user profile photos. mtrace("Started - Syncing user profile images."); $countsyncedimages = $this->sync_profile_images(); mtrace("Completed - Synced $countsyncedimages profile images."); } } /** * Process unenrolment of users for a given resource link and based on the list of recently synced users. * * @param resource_link $resourcelink the resource_link instance to which the $synced users pertains * @param stdClass $resource the resource object instance * @param array $syncedusers the array of recently synced users, who are not to be unenrolled. * @return int the number of unenrolled users. */ protected function sync_unenrol_resourcelink(resource_link $resourcelink, stdClass $resource, array $syncedusers): int { if (!$this->should_sync_unenrol($resource->membersyncmode)) { return 0; } $ltiplugin = enrol_get_plugin('lti'); $unenrolcount = 0; // Get all users for the resource_link instance. $linkusers = $this->userrepo->find_by_resource_link($resourcelink->get_id()); foreach ($linkusers as $ltiuser) { if (!in_array($ltiuser->get_localid(), $syncedusers)) { $instance = new stdClass(); $instance->id = $resource->enrolid; $instance->courseid = $resource->courseid; $instance->enrol = 'lti'; $ltiplugin->unenrol_user($instance, $ltiuser->get_localid()); $unenrolcount++; } } return $unenrolcount; } /** * Check whether the member has an instructor role or not. * * @param array $member * @return bool */ protected function member_is_instructor(array $member): bool { // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. $memberroles = $member['roles']; if ($memberroles) { $adminroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator', 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator' ]; $staffroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant', 'ContentDeveloper', 'Instructor', 'Instructor#TeachingAssistant' ]; $instructorroles = array_merge($adminroles, $staffroles); foreach ($instructorroles as $validrole) { if (in_array($validrole, $memberroles)) { return true; } } } return false; } /** * Method to determine whether to sync unenrolments or not. * * @param int $syncmode The shared resource's membersyncmode. * @return bool true if unenrolment should be synced, false if not. */ protected function should_sync_unenrol($syncmode): bool { return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING; } /** * Method to determine whether to sync enrolments or not. * * @param int $syncmode The shared resource's membersyncmode. * @return bool true if enrolment should be synced, false if not. */ protected function should_sync_enrol($syncmode): bool { return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW; } /** * Creates an lti user object from a member entry. * * @param stdClass $user the Moodle user record representing this member. * @param stdClass $resource the locally published resource record, used for setting user defaults. * @param resource_link $resourcelink the resource_link instance. * @param array $member the member information from the NRPS service call. * @return user the lti user instance. */ protected function ltiuser_from_member(stdClass $user, stdClass $resource, resource_link $resourcelink, array $member): user { if (!$ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) { // New user, so create them. $ltiuser = user::create( $resourcelink->get_resourceid(), $user->id, $resourcelink->get_deploymentid(), $member['user_id'], $resource->lang, $resource->timezone, $resource->city ?? '', $resource->country ?? '', $resource->institution ?? '', $resource->maildisplay ); } $ltiuser->set_lastaccess(time()); return $ltiuser; } /** * Performs synchronisation of member information and enrolments. * * @param application_registration $appregistration the application_registration instance. * @param stdClass $resource the enrol_lti_tools resource information. * @param resource_link $resourcelink the resource_link instance. * @param user[] $members an array of members to sync. * @return array An array containing the counts of enrolled users and a list of userids. */ protected function sync_member_information(application_registration $appregistration, stdClass $resource, resource_link $resourcelink, array $members): array { $enrolcount = 0; $userids = []; // Get the verified legacy consumer key, if mapped, from the resource link's tool deployment. // This will be used to locate legacy user accounts and link them to LTI 1.3 users. // A launch must have been made in order to get the legacy consumer key from the lti1p1 migration claim. $deployment = $this->deploymentrepo->find($resourcelink->get_deploymentid()); $legacyconsumerkey = $deployment->get_legacy_consumer_key() ?? ''; foreach ($members as $member) { $auth = get_auth_plugin('lti'); if ($auth->get_user_binding($appregistration->get_platformid()->out(false), $member['user_id'])) { // Use is bound already, so we can update them. $user = $auth->find_or_create_user_from_membership($member, $appregistration->get_platformid()->out(false)); if ($user->auth != 'lti') { mtrace("Skipped profile sync for user '$user->id'. The user does not belong to the LTI auth method."); } } else { // Not bound, so defer to the role-based provisioning mode for the resource. $provisioningmode = $this->member_is_instructor($member) ? $resource->provisioningmodeinstructor : $resource->provisioningmodelearner; switch ($provisioningmode) { case \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY: // Automatic provisioning - this will create a user account and log the user in. $user = $auth->find_or_create_user_from_membership($member, $appregistration->get_platformid()->out(false), $legacyconsumerkey); break; case \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING: case \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY: default: mtrace("Skipping account creation for member '{$member['user_id']}'. This member is not eligible for ". "automatic creation due to the current account provisioning mode."); continue 2; } } $ltiuser = $this->ltiuser_from_member($user, $resource, $resourcelink, $member); if ($this->should_sync_enrol($resource->membersyncmode)) { $ltiuser->set_resourcelinkid($resourcelink->get_id()); $ltiuser = $this->userrepo->save($ltiuser); if ($user->auth != 'lti') { mtrace("Skipped picture sync for user '$user->id'. The user does not belong to the LTI auth method."); } else { if (isset($member['picture'])) { $this->userphotos[$ltiuser->get_localid()] = $member['picture']; } } // Enrol the user in the course. if (helper::enrol_user($resource, $ltiuser->get_localid()) === helper::ENROLMENT_SUCCESSFUL) { $enrolcount++; } } // If the member has been created, or exists locally already, mark them as valid so as to not unenrol them // when syncing memberships for shared resources configured as either MEMBER_SYNC_ENROL_AND_UNENROL or // MEMBER_SYNC_UNENROL_MISSING. $userids[] = $user->id; } return [$enrolcount, $userids]; } /** * Performs synchronisation of user profile images. * * @return int the count of synced photos. */ protected function sync_profile_images(): int { $counter = 0; foreach ($this->userphotos as $userid => $url) { if ($url) { $result = helper::update_user_profile_image($userid, $url); if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) { $counter++; mtrace("Profile image successfully downloaded and created for user '$userid' from $url."); } else { mtrace($result); } } } return $counter; } } classes/local/ltiadvantage/table/published_resources_table.php000064400000021213152266447110020765 0ustar00. namespace enrol_lti\local\ltiadvantage\table; use enrol_lti\helper; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir . '/tablelib.php'); /** * Class which displays a list of resources published over LTI Advantage. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class published_resources_table extends \table_sql { /** * @var \enrol_plugin $ltiplugin */ protected $ltiplugin; /** * @var bool $ltienabled */ protected $ltienabled; /** * @var bool $canconfig */ protected $canconfig; /** * @var int $courseid The course id. */ protected $courseid; /** * Sets up the table. * * @param string $courseid The id of the course. */ public function __construct($courseid) { parent::__construct('enrol_lti_manage_table'); $this->define_columns(array( 'name', 'launch', 'edit' )); $this->define_headers(array( get_string('name'), get_string('lti13launchdetails', 'enrol_lti'), get_string('edit') )); $this->collapsible(false); $this->sortable(false); // Set the variables we need access to. $this->ltiplugin = enrol_get_plugin('lti'); $this->ltienabled = enrol_is_enabled('lti'); $this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid)); $this->courseid = $courseid; // Set help icons. $launchicon = new \help_icon('lti13launchdetails', 'enrol_lti'); $this->define_help_for_headers(['1' => $launchicon]); } /** * Generate the name column. * * @param \stdClass $tool event data. * @return string */ public function col_name($tool) { $toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING); $name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool); return $this->get_display_text($tool, $name); } /** * Generate the launch column. * * @param \stdClass $tool instance data. * @return string */ public function col_launch($tool) { global $OUTPUT; $customparamslabel = get_string('customproperties', 'enrol_lti'); $customparams = "id={$tool->uuid}"; $launchurl = new \moodle_url('/enrol/lti/launch.php'); $launchurllabel = get_string('launchurl', 'enrol_lti'); $data = [ "rows" => [ [ "label" => $launchurllabel, "text" => $launchurl->out(false), "id" => "launchurl", "hidelabel" => false ], [ "label" => $customparamslabel, "text" => $customparams, "id" => "customparams", "hidelabel" => false ] ] ]; $return = $OUTPUT->render_from_template("enrol_lti/copy_grid", $data); return $return; } /** * Generate the edit column. * * @param \stdClass $tool event data. * @return string */ public function col_edit($tool) { global $OUTPUT; $buttons = array(); $instance = new \stdClass(); $instance->id = $tool->enrolid; $instance->courseid = $tool->courseid; $instance->enrol = 'lti'; $instance->status = $tool->status; $strdelete = get_string('delete'); $strenable = get_string('enable'); $strdisable = get_string('disable'); $url = new \moodle_url('/enrol/lti/index.php', array('sesskey' => sesskey(), 'courseid' => $this->courseid)); if ($this->ltiplugin->can_delete_instance($instance)) { $aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core', array('class' => 'iconsmall'))); } if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) { if ($instance->status == ENROL_INSTANCE_ENABLED) { $aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core', array('class' => 'iconsmall'))); } else if ($instance->status == ENROL_INSTANCE_DISABLED) { $aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id)); $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core', array('class' => 'iconsmall'))); } } if ($this->ltienabled && $this->canconfig) { $linkparams = array( 'courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol, 'returnurl' => new \moodle_url('/enrol/lti/index.php', array('courseid' => $this->courseid)) ); $editlink = new \moodle_url("/enrol/editinstance.php", $linkparams); $buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core', array('class' => 'iconsmall'))); } return implode(' ', $buttons); } /** * Query the reader. Store results in the object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. */ public function query_db($pagesize, $useinitialsbar = true) { $total = helper::count_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p3']); $this->pagesize($pagesize, $total); $tools = helper::get_lti_tools(['courseid' => $this->courseid, 'ltiversion' => 'LTI-1p3'], $this->get_page_start(), $this->get_page_size()); $this->rawdata = $tools; // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Returns text to display in the columns. * * @param \stdClass $tool the tool * @param string $text the text to alter * @return string */ protected function get_display_text($tool, $text) { if ($tool->status != ENROL_INSTANCE_ENABLED) { return \html_writer::tag('div', $text, array('class' => 'dimmed_text')); } return $text; } /** * Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted. * * @param \stdClass $tool the tool instance record. * @return string the HTML for the name column. */ protected function get_deleted_activity_name_html(\stdClass $tool): string { global $OUTPUT; $icon = \html_writer::tag( 'a', $OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [ "class" => "btn btn-link p-0", "role" => "button", "data-bs-container" => "body", "data-bs-toggle" => "popover", "data-bs-placement" => right_to_left() ? "left" : "right", "data-bs-content" => get_string('deletedactivitydescription', 'enrol_lti'), "data-bs-html" => "true", "tabindex" => "0", "data-bs-trigger" => "focus", ] ); $name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti')); if ($tool->name) { $name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name; } return $name; } } classes/local/ltiadvantage/utility/message_helper.php000064400000007361152266447110017154 0ustar00. namespace enrol_lti\local\ltiadvantage\utility; /** * Utility class for LTI Advantage messages. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class message_helper { /** * Determine if the LTI roles in the launch contains any instructor or admin roles. * * @param array $jwtdata array formatted JWT data from the launch. * @return bool true if the roles contain a constructor role, false otherwise. */ public static function is_instructor_launch(array $jwtdata): bool { return self::user_is_admin($jwtdata) || self::user_is_staff($jwtdata, true); } /** * Check whether the launch user is an instructor. * * @param array $jwtdata array formatted JWT data from the launch. * @param bool $includelegacyroles whether to also consider legacy simple names as valid roles. * @return bool true if the user is an instructor, false otherwise. */ private static function user_is_staff(array $jwtdata, bool $includelegacyroles = false): bool { // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. // This method also provides support for (legacy, deprecated) simple names for context roles. // I.e. 'ContentDeveloper' may be supported. $launchroles = $jwtdata['https://purl.imsglobal.org/spec/lti/claim/roles'] ?? null; if ($launchroles) { $staffroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant' ]; if ($includelegacyroles) { $staffroles[] = 'ContentDeveloper'; $staffroles[] = 'Instructor'; $staffroles[] = 'Instructor#TeachingAssistant'; } foreach ($staffroles as $validrole) { if (in_array($validrole, $launchroles)) { return true; } } } return false; } /** * Check whether the launch user has an admin role. * * @param array $jwtdata array formatted JWT data from the launch. * @return bool true if the user is admin, false otherwise. */ private static function user_is_admin(array $jwtdata): bool { // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. $launchroles = $jwtdata['https://purl.imsglobal.org/spec/lti/claim/roles'] ?? null; if ($launchroles) { $adminroles = [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator', 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator' ]; foreach ($adminroles as $validrole) { if (in_array($validrole, $launchroles)) { return true; } } } return false; } } classes/tool_provider.php000064400000041651152266447110011620 0ustar00. /** * Extends the IMS Tool provider library for the LTI enrolment. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti; defined('MOODLE_INTERNAL') || die; use context; use core\notification; use core_user; use enrol_lti\output\registration; use html_writer; use IMSGlobal\LTI\Profile\Item; use IMSGlobal\LTI\Profile\Message; use IMSGlobal\LTI\Profile\ResourceHandler; use IMSGlobal\LTI\Profile\ServiceDefinition; use IMSGlobal\LTI\ToolProvider\ToolProvider; use moodle_exception; use moodle_url; use stdClass; require_once($CFG->dirroot . '/user/lib.php'); /** * Extends the IMS Tool provider library for the LTI enrolment. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_provider extends ToolProvider { /** * @var stdClass $tool The object representing the enrol instance providing this LTI tool */ protected $tool; /** * Remove $this->baseUrl (wwwroot) from a given url string and return it. * * @param string $url The url from which to remove the base url * @return string|null A string of the relative path to the url, or null if it couldn't be determined. */ protected function strip_base_url($url) { if (substr($url, 0, strlen($this->baseUrl)) == $this->baseUrl) { return substr($url, strlen($this->baseUrl)); } return null; } /** * Create a new instance of tool_provider to handle all the LTI tool provider interactions. * * @param int $toolid The id of the tool to be provided. */ public function __construct($toolid) { global $CFG, $SITE; $token = helper::generate_proxy_token($toolid); $tool = helper::get_lti_tool($toolid); $this->tool = $tool; $dataconnector = new data_connector(); parent::__construct($dataconnector); // Override debugMode and set to the configured value. $this->debugMode = $CFG->debugdeveloper; $this->baseUrl = $CFG->wwwroot; $toolpath = helper::get_launch_url($toolid); $toolpath = $this->strip_base_url($toolpath); $vendorid = $SITE->shortname; $vendorname = $SITE->fullname; $vendordescription = trim(html_to_text($SITE->summary)); $this->vendor = new Item($vendorid, $vendorname, $vendordescription, $CFG->wwwroot); $name = helper::get_name($tool); $description = helper::get_description($tool); $icon = helper::get_icon($tool)->out(); $icon = $this->strip_base_url($icon); $this->product = new Item( $token, $name, $description, helper::get_proxy_url($tool), '1.0' ); $requiredmessages = [ new Message( 'basic-lti-launch-request', $toolpath, [ 'Context.id', 'CourseSection.title', 'CourseSection.label', 'CourseSection.sourcedId', 'CourseSection.longDescription', 'CourseSection.timeFrame.begin', 'ResourceLink.id', 'ResourceLink.title', 'ResourceLink.description', 'User.id', 'User.username', 'Person.name.full', 'Person.name.given', 'Person.name.family', 'Person.email.primary', 'Person.sourcedId', 'Person.name.middle', 'Person.address.street1', 'Person.address.locality', 'Person.address.country', 'Person.address.timezone', 'Person.phone.primary', 'Person.phone.mobile', 'Person.webaddress', 'Membership.role', 'Result.sourcedId', 'Result.autocreate' ] ) ]; $optionalmessages = [ ]; $this->resourceHandlers[] = new ResourceHandler( new Item( $token, helper::get_name($tool), $description ), $icon, $requiredmessages, $optionalmessages ); $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lti.v2.toolproxy+json'], ['POST']); $this->requiredServices[] = new ServiceDefinition(['application/vnd.ims.lis.v2.membershipcontainer+json'], ['GET']); } /** * Override onError for custom error handling. * @return void */ protected function onError() { global $OUTPUT; $message = $this->message; if ($this->debugMode && !empty($this->reason)) { $message = $this->reason; } // Display the error message from the provider's side if the consumer has not specified a URL to pass the error to. if (empty($this->returnUrl)) { $this->errorOutput = $OUTPUT->notification(get_string('failedrequest', 'enrol_lti', ['reason' => $message]), 'error'); } } /** * Override onLaunch with tool logic. * @return void */ protected function onLaunch() { global $DB, $SESSION, $CFG; // Check for valid consumer. if (empty($this->consumer) || $this->dataConnector->loadToolConsumer($this->consumer) === false) { $this->ok = false; $this->message = get_string('invalidtoolconsumer', 'enrol_lti'); return; } $url = helper::get_launch_url($this->tool->id); // If a tool proxy has been stored for the current consumer trying to access a tool, // check that the tool is being launched from the correct url. $correctlaunchurl = false; if (!empty($this->consumer->toolProxy)) { $proxy = json_decode($this->consumer->toolProxy); $handlers = $proxy->tool_profile->resource_handler; foreach ($handlers as $handler) { foreach ($handler->message as $message) { $handlerurl = new moodle_url($message->path); $fullpath = $handlerurl->out(false); if ($message->message_type == "basic-lti-launch-request" && $fullpath == $url) { $correctlaunchurl = true; break 2; } } } } else if ($this->tool->secret == $this->consumer->secret) { // Test if the LTI1 secret for this tool is being used. Then we know the correct tool is being launched. $correctlaunchurl = true; } if (!$correctlaunchurl) { $this->ok = false; $this->message = get_string('invalidrequest', 'enrol_lti'); return; } // Before we do anything check that the context is valid. $tool = $this->tool; $context = context::instance_by_id($tool->contextid); // Set the user data. $user = new stdClass(); $user->username = helper::create_username($this->consumer->getKey(), $this->user->ltiUserId); if (!empty($this->user->firstname)) { $user->firstname = $this->user->firstname; } else { $user->firstname = $this->user->getRecordId(); } if (!empty($this->user->lastname)) { $user->lastname = $this->user->lastname; } else { $user->lastname = $this->tool->contextid; } $user->email = core_user::clean_field($this->user->email, 'email'); // Get the user data from the LTI consumer. $user = helper::assign_user_tool_data($tool, $user); // Check if the user exists. if (!$dbuser = $DB->get_record('user', ['username' => $user->username, 'deleted' => 0])) { // If the email was stripped/not set then fill it with a default one. This // stops the user from being redirected to edit their profile page. if (empty($user->email)) { $user->email = $user->username . "@example.com"; } $user->auth = 'lti'; $user->id = \user_create_user($user); // Get the updated user record. $user = $DB->get_record('user', ['id' => $user->id]); } else { if ($dbuser->suspended) { require_once($CFG->libdir . '/authlib.php'); $failurereason = AUTH_LOGIN_SUSPENDED; $event = \core\event\user_login_failed::create([ 'userid' => $dbuser->id, 'other' => [ 'username' => $dbuser->username, 'reason' => $failurereason ] ]); $event->trigger(); throw new \core\exception\moodle_exception('invalidlogin', 'core'); } if (helper::user_match($user, $dbuser)) { $user = $dbuser; } else { // If email is empty remove it, so we don't update the user with an empty email. if (empty($user->email)) { unset($user->email); } $user->id = $dbuser->id; \user_update_user($user); // Get the updated user record. $user = $DB->get_record('user', ['id' => $user->id]); } } // Update user image. if (isset($this->user) && isset($this->user->image) && !empty($this->user->image)) { $image = $this->user->image; } else { // Use custom_user_image parameter as a fallback. $image = $this->resourceLink->getSetting('custom_user_image'); } // Check if there is an image to process. if ($image) { helper::update_user_profile_image($user->id, $image); } // Check if we need to force the page layout to embedded. $isforceembed = $this->resourceLink->getSetting('custom_force_embed') == 1; // Check if we are an instructor. $isinstructor = $this->user->isStaff() || $this->user->isAdmin(); if ($context->contextlevel == CONTEXT_COURSE) { $courseid = $context->instanceid; $urltogo = new moodle_url('/course/view.php', ['id' => $courseid]); } else if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); $urltogo = new moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]); // If we are a student in the course module context we do not want to display blocks. if (!$isforceembed && !$isinstructor) { $isforceembed = true; } } else { throw new \moodle_exception('invalidcontext'); exit(); } // Force page layout to embedded if necessary. if ($isforceembed) { $SESSION->forcepagelayout = 'embedded'; } else { // May still be set from previous session, so unset it. unset($SESSION->forcepagelayout); } // Enrol the user in the course with no role. $result = helper::enrol_user($tool, $user->id); // Display an error, if there is one. if ($result !== helper::ENROLMENT_SUCCESSFUL) { throw new \moodle_exception($result, 'enrol_lti'); exit(); } // Give the user the role in the given context. $roleid = $isinstructor ? $tool->roleinstructor : $tool->rolelearner; role_assign($roleid, $user->id, $tool->contextid); // Login user. $sourceid = $this->user->ltiResultSourcedId; $serviceurl = $this->resourceLink->getSetting('lis_outcome_service_url'); // Check if we have recorded this user before. if ($userlog = $DB->get_record('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) { if ($userlog->sourceid != $sourceid) { $userlog->sourceid = $sourceid; } if ($userlog->serviceurl != $serviceurl) { $userlog->serviceurl = $serviceurl; } if (empty($userlog->consumersecret)) { $userlog->consumersecret = $this->consumer->secret; } $userlog->lastaccess = time(); $DB->update_record('enrol_lti_users', $userlog); } else { // Add the user details so we can use it later when syncing grades and members. $userlog = new stdClass(); $userlog->userid = $user->id; $userlog->toolid = $tool->id; $userlog->serviceurl = $serviceurl; $userlog->sourceid = $sourceid; $userlog->consumerkey = $this->consumer->getKey(); $userlog->consumersecret = $this->consumer->secret; $userlog->lastgrade = 0; $userlog->lastaccess = time(); $userlog->timecreated = time(); $userlog->membershipsurl = $this->resourceLink->getSetting('ext_ims_lis_memberships_url'); $userlog->membershipsid = $this->resourceLink->getSetting('ext_ims_lis_memberships_id'); $DB->insert_record('enrol_lti_users', $userlog); } // Finalise the user log in. complete_user_login($user); // Everything's good. Set appropriate OK flag and message values. $this->ok = true; $this->message = get_string('success'); if (empty($CFG->allowframembedding)) { // Provide an alternative link. $stropentool = get_string('opentool', 'enrol_lti'); echo html_writer::tag('p', get_string('frameembeddingnotenabled', 'enrol_lti')); echo html_writer::link($urltogo, $stropentool, ['target' => '_blank']); } else { // All done, redirect the user to where they want to go. redirect($urltogo); } } /** * Override onRegister with registration code. */ protected function onRegister() { global $PAGE; if (empty($this->consumer)) { $this->ok = false; $this->message = get_string('invalidtoolconsumer', 'enrol_lti'); return; } if (empty($this->returnUrl)) { $this->ok = false; $this->message = get_string('returnurlnotset', 'enrol_lti'); return; } if ($this->doToolProxyService()) { // Map tool consumer and published tool, if necessary. $this->map_tool_to_consumer(); // Indicate successful processing in message. $this->message = get_string('successfulregistration', 'enrol_lti'); // Prepare response. $returnurl = new moodle_url($this->returnUrl); $returnurl->param('lti_msg', get_string("successfulregistration", "enrol_lti")); $returnurl->param('status', 'success'); $guid = $this->consumer->getKey(); $returnurl->param('tool_proxy_guid', $guid); $returnurlout = $returnurl->out(false); $registration = new registration($returnurlout); $output = $PAGE->get_renderer('enrol_lti'); echo $output->render($registration); } else { // Tell the consumer that the registration failed. $this->ok = false; $this->message = get_string('couldnotestablishproxy', 'enrol_lti'); } } /** * Performs mapping of the tool consumer to a published tool. * * @throws moodle_exception */ public function map_tool_to_consumer() { global $DB; if (empty($this->consumer)) { throw new moodle_exception('invalidtoolconsumer', 'enrol_lti'); } // Map the consumer to the tool. $mappingparams = [ 'toolid' => $this->tool->id, 'consumerid' => $this->consumer->getRecordId() ]; $mappingexists = $DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams); if (!$mappingexists) { $DB->insert_record('enrol_lti_tool_consumer_map', (object) $mappingparams); } } } classes/data_connector.php000064400000105332152266447110011711 0ustar00. /** * Extends the IMS Tool provider library data connector for moodle. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti; defined('MOODLE_INTERNAL') || die; use IMSGlobal\LTI\ToolProvider; use IMSGlobal\LTI\ToolProvider\ConsumerNonce; use IMSGlobal\LTI\ToolProvider\Context; use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector; use IMSGlobal\LTI\ToolProvider\ResourceLink; use IMSGlobal\LTI\ToolProvider\ResourceLinkShare; use IMSGlobal\LTI\ToolProvider\ResourceLinkShareKey; use IMSGlobal\LTI\ToolProvider\ToolConsumer; use IMSGlobal\LTI\ToolProvider\ToolProxy; use IMSGlobal\LTI\ToolProvider\User; use stdClass; /** * Extends the IMS Tool provider library data connector for moodle. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_connector extends DataConnector { /** @var string Tool consumer table name. */ protected $consumertable; /** @var string Context table name. */ protected $contexttable; /** @var string Consumer nonce table name. */ protected $noncetable; /** @var string Resource link table name. */ protected $resourcelinktable; /** @var string Resource link share key table name. */ protected $sharekeytable; /** @var string Tool proxy table name. */ protected $toolproxytable; /** @var string User result table name. */ protected $userresulttable; /** * data_connector constructor. */ public function __construct() { parent::__construct(null, 'enrol_lti_'); // Set up table names. $this->consumertable = $this->dbTableNamePrefix . DataConnector::CONSUMER_TABLE_NAME; $this->contexttable = $this->dbTableNamePrefix . DataConnector::CONTEXT_TABLE_NAME; $this->noncetable = $this->dbTableNamePrefix . DataConnector::NONCE_TABLE_NAME; $this->resourcelinktable = $this->dbTableNamePrefix . DataConnector::RESOURCE_LINK_TABLE_NAME; $this->sharekeytable = $this->dbTableNamePrefix . DataConnector::RESOURCE_LINK_SHARE_KEY_TABLE_NAME; $this->toolproxytable = $this->dbTableNamePrefix . DataConnector::TOOL_PROXY_TABLE_NAME; $this->userresulttable = $this->dbTableNamePrefix . DataConnector::USER_RESULT_TABLE_NAME; } /** * Load tool consumer object. * * @param ToolConsumer $consumer ToolConsumer object * @return boolean True if the tool consumer object was successfully loaded */ public function loadToolConsumer($consumer) { global $DB; $id = $consumer->getRecordId(); $key = $consumer->getKey(); $result = false; if (!empty($id)) { $result = $DB->get_record($this->consumertable, ['id' => $id]); } else if (!empty($key)) { $key256 = DataConnector::getConsumerKey($key); $result = $DB->get_record($this->consumertable, ['consumerkey256' => $key256]); } if ($result) { if (empty($key256) || empty($result->consumerkey) || ($key === $result->consumerkey)) { $this->build_tool_consumer_object($result, $consumer); return true; } } return false; } /** * Save tool consumer object. * * @param ToolConsumer $consumer Consumer object * @return boolean True if the tool consumer object was successfully saved */ public function saveToolConsumer($consumer) { global $DB; $key = $consumer->getKey(); $key256 = DataConnector::getConsumerKey($key); if ($key === $key256) { $key = null; } $protected = ($consumer->protected) ? 1 : 0; $enabled = ($consumer->enabled) ? 1 : 0; $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; $settingsvalue = serialize($consumer->getSettings()); $now = time(); $consumer->updated = $now; $data = [ 'consumerkey256' => $key256, 'consumerkey' => $key, 'name' => $consumer->name, 'secret' => $consumer->secret, 'ltiversion' => $consumer->ltiVersion, 'consumername' => $consumer->consumerName, 'consumerversion' => $consumer->consumerVersion, 'consumerguid' => $consumer->consumerGuid, 'profile' => $profile, 'toolproxy' => $consumer->toolProxy, 'settings' => $settingsvalue, 'protected' => $protected, 'enabled' => $enabled, 'enablefrom' => $consumer->enableFrom, 'enableuntil' => $consumer->enableUntil, 'lastaccess' => $consumer->lastAccess, 'updated' => $consumer->updated, ]; $id = $consumer->getRecordId(); if (empty($id)) { $consumer->created = $now; $data['created'] = $consumer->created; $id = $DB->insert_record($this->consumertable, (object) $data); if ($id) { $consumer->setRecordId($id); return true; } } else { $data['id'] = $id; return $DB->update_record($this->consumertable, (object) $data); } return false; } /** * Delete tool consumer object and related records. * * @param ToolConsumer $consumer Consumer object * @return boolean True if the tool consumer object was successfully deleted */ public function deleteToolConsumer($consumer) { global $DB; $consumerpk = $consumer->getRecordId(); $deletecondition = ['consumerid' => $consumerpk]; // Delete any nonce values for this consumer. $DB->delete_records($this->noncetable, $deletecondition); // Delete any outstanding share keys for resource links for this consumer. $where = "resourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl WHERE rl.consumerid = :consumerid )"; $DB->delete_records_select($this->sharekeytable, $where, $deletecondition); // Delete any outstanding share keys for resource links for contexts in this consumer. $where = "resourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl INNER JOIN {{$this->contexttable}} c ON rl.contextid = c.id WHERE c.consumerid = :consumerid )"; $DB->delete_records_select($this->sharekeytable, $where, $deletecondition); // Delete any users in resource links for this consumer. $where = "resourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl WHERE rl.consumerid = :consumerid )"; $DB->delete_records_select($this->userresulttable, $where, $deletecondition); // Delete any users in resource links for contexts in this consumer. $where = "resourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl INNER JOIN {{$this->contexttable}} c ON rl.contextid = c.id WHERE c.consumerid = :consumerid )"; $DB->delete_records_select($this->userresulttable, $where, $deletecondition); // Update any resource links for which this consumer is acting as a primary resource link. $where = "primaryresourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl WHERE rl.consumerid = :consumerid )"; $updaterecords = $DB->get_records_select($this->resourcelinktable, $where, $deletecondition); foreach ($updaterecords as $record) { $record->primaryresourcelinkid = null; $record->shareapproved = null; $DB->update_record($this->resourcelinktable, $record); } // Update any resource links for contexts in which this consumer is acting as a primary resource link. $where = "primaryresourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl INNER JOIN {{$this->contexttable}} c ON rl.contextid = c.id WHERE c.consumerid = :consumerid )"; $updaterecords = $DB->get_records_select($this->resourcelinktable, $where, $deletecondition); foreach ($updaterecords as $record) { $record->primaryresourcelinkid = null; $record->shareapproved = null; $DB->update_record($this->resourcelinktable, $record); } // Delete any resource links for contexts in this consumer. $where = "contextid IN ( SELECT c.id FROM {{$this->contexttable}} c WHERE c.consumerid = :consumerid )"; $DB->delete_records_select($this->resourcelinktable, $where, $deletecondition); // Delete any resource links for this consumer. $DB->delete_records($this->resourcelinktable, $deletecondition); // Delete any contexts for this consumer. $DB->delete_records($this->contexttable, $deletecondition); // Delete consumer. $DB->delete_records($this->consumertable, ['id' => $consumerpk]); $consumer->initialize(); return true; } /** * Load all tool consumers from the database. * @return array */ public function getToolConsumers() { global $DB; $consumers = []; $rsconsumers = $DB->get_recordset($this->consumertable, null, 'name'); foreach ($rsconsumers as $row) { $consumer = new ToolProvider\ToolConsumer($row->consumerkey, $this); $this->build_tool_consumer_object($row, $consumer); $consumers[] = $consumer; } $rsconsumers->close(); return $consumers; } /* * ToolProxy methods. */ /** * Load the tool proxy from the database. * * @param ToolProxy $toolproxy * @return bool */ public function loadToolProxy($toolproxy) { return false; } /** * Save the tool proxy to the database. * * @param ToolProxy $toolproxy * @return bool */ public function saveToolProxy($toolproxy) { return false; } /** * Delete the tool proxy from the database. * * @param ToolProxy $toolproxy * @return bool */ public function deleteToolProxy($toolproxy) { return false; } /* * Context methods. */ /** * Load context object. * * @param Context $context Context object * @return boolean True if the context object was successfully loaded */ public function loadContext($context) { global $DB; if (!empty($context->getRecordId())) { $params = ['id' => $context->getRecordId()]; } else { $params = [ 'consumerid' => $context->getConsumer()->getRecordId(), 'lticontextkey' => $context->ltiContextId ]; } if ($row = $DB->get_record($this->contexttable, $params)) { $context->setRecordId($row->id); $context->setConsumerId($row->consumerid); $context->ltiContextId = $row->lticontextkey; $context->type = $row->type; $settings = unserialize($row->settings); if (!is_array($settings)) { $settings = array(); } $context->setSettings($settings); $context->created = $row->created; $context->updated = $row->updated; return true; } return false; } /** * Save context object. * * @param Context $context Context object * @return boolean True if the context object was successfully saved */ public function saveContext($context) { global $DB; $now = time(); $context->updated = $now; $settingsvalue = serialize($context->getSettings()); $id = $context->getRecordId(); $consumerpk = $context->getConsumer()->getRecordId(); $isinsert = empty($id); if ($isinsert) { $context->created = $now; $params = [ 'consumerid' => $consumerpk, 'lticontextkey' => $context->ltiContextId, 'type' => $context->type, 'settings' => $settingsvalue, 'created' => $context->created, 'updated' => $context->updated, ]; $id = $DB->insert_record($this->contexttable, (object) $params); if ($id) { $context->setRecordId($id); return true; } } else { $data = (object) [ 'id' => $id, 'contextid' => $consumerpk, 'lticontextkey' => $context->ltiContextId, 'type' => $context->type, 'settings' => $settingsvalue, 'updated' => $context->updated, ]; return $DB->update_record($this->contexttable, $data); } return false; } /** * Delete context object. * * @param Context $context Context object * @return boolean True if the Context object was successfully deleted */ public function deleteContext($context) { global $DB; $contextid = $context->getRecordId(); $params = ['id' => $contextid]; // Delete any outstanding share keys for resource links for this context. $where = "resourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl WHERE rl.contextid = :id )"; $DB->delete_records_select($this->sharekeytable, $where, $params); // Delete any users in resource links for this context. $DB->delete_records_select($this->userresulttable, $where, $params); // Update any resource links for which this consumer is acting as a primary resource link. $where = "primaryresourcelinkid IN ( SELECT rl.id FROM {{$this->resourcelinktable}} rl WHERE rl.contextid = :id )"; $updaterecords = $DB->get_records_select($this->resourcelinktable, $where, $params); foreach ($updaterecords as $record) { $record->primaryresourcelinkid = null; $record->shareapproved = null; $DB->update_record($this->resourcelinktable, $record); } // Delete any resource links for this context. $DB->delete_records($this->resourcelinktable, ['contextid' => $contextid]); // Delete context. $DB->delete_records($this->contexttable, $params); $context->initialize(); return true; } /* * ResourceLink methods */ /** * Load resource link object. * * @param ResourceLink $resourcelink ResourceLink object * @return boolean True if the resource link object was successfully loaded */ public function loadResourceLink($resourcelink) { global $DB; $resourceid = $resourcelink->getRecordId(); if (!empty($resourceid)) { $params = ['id' => $resourceid]; $row = $DB->get_record($this->resourcelinktable, $params); } else if (!empty($resourcelink->getContext())) { $params = [ 'contextid' => $resourcelink->getContext()->getRecordId(), 'ltiresourcelinkkey' => $resourcelink->getId() ]; $row = $DB->get_record($this->resourcelinktable, $params); } else { $sql = "SELECT r.* FROM {{$this->resourcelinktable}} r LEFT OUTER JOIN {{$this->contexttable}} c ON r.contextid = c.id WHERE (r.consumerid = ? OR c.consumerid = ?) AND ltiresourcelinkkey = ?"; $params = [ $resourcelink->getConsumer()->getRecordId(), $resourcelink->getConsumer()->getRecordId(), $resourcelink->getId() ]; $row = $DB->get_record_sql($sql, $params); } if ($row) { $resourcelink->setRecordId($row->id); if (!is_null($row->contextid)) { $resourcelink->setContextId($row->contextid); } else { $resourcelink->setContextId(null); } if (!is_null($row->consumerid)) { $resourcelink->setConsumerId($row->consumerid); } else { $resourcelink->setConsumerId(null); } $resourcelink->ltiResourceLinkId = $row->ltiresourcelinkkey; $settings = unserialize($row->settings); if (!is_array($settings)) { $settings = array(); } $resourcelink->setSettings($settings); if (!is_null($row->primaryresourcelinkid)) { $resourcelink->primaryResourceLinkId = $row->primaryresourcelinkid; } else { $resourcelink->primaryResourceLinkId = null; } $resourcelink->shareApproved = (is_null($row->shareapproved)) ? null : ($row->shareapproved == 1); $resourcelink->created = $row->created; $resourcelink->updated = $row->updated; return true; } return false; } /** * Save resource link object. * * @param ResourceLink $resourcelink Resource_Link object * @return boolean True if the resource link object was successfully saved */ public function saveResourceLink($resourcelink) { global $DB; if (is_null($resourcelink->shareApproved)) { $approved = null; } else if ($resourcelink->shareApproved) { $approved = 1; } else { $approved = 0; } if (empty($resourcelink->primaryResourceLinkId)) { $primaryresourcelinkid = null; } else { $primaryresourcelinkid = $resourcelink->primaryResourceLinkId; } $now = time(); $resourcelink->updated = $now; $settingsvalue = serialize($resourcelink->getSettings()); if (!empty($resourcelink->getContext())) { $consumerid = null; $contextid = $resourcelink->getContext()->getRecordId(); } else if (!empty($resourcelink->getContextId())) { $consumerid = null; $contextid = $resourcelink->getContextId(); } else { $consumerid = $resourcelink->getConsumer()->getRecordId(); $contextid = null; } $id = $resourcelink->getRecordId(); $data = [ 'consumerid' => $consumerid, 'contextid' => $contextid, 'ltiresourcelinkkey' => $resourcelink->getId(), 'settings' => $settingsvalue, 'primaryresourcelinkid' => $primaryresourcelinkid, 'shareapproved' => $approved, 'updated' => $resourcelink->updated, ]; $returnid = null; if (empty($id)) { $resourcelink->created = $now; $data['created'] = $resourcelink->created; $id = $DB->insert_record($this->resourcelinktable, (object) $data); if ($id) { $resourcelink->setRecordId($id); return true; } } else { $data['id'] = $id; return $DB->update_record($this->resourcelinktable, (object) $data); } return false; } /** * Delete resource link object. * * @param ResourceLink $resourcelink ResourceLink object * @return boolean True if the resource link object and its related records were successfully deleted. * Otherwise, a DML exception is thrown. */ public function deleteResourceLink($resourcelink) { global $DB; $resourcelinkid = $resourcelink->getRecordId(); // Delete any outstanding share keys for resource links for this consumer. $DB->delete_records($this->sharekeytable, ['resourcelinkid' => $resourcelinkid]); // Delete users. $DB->delete_records($this->userresulttable, ['resourcelinkid' => $resourcelinkid]); // Update any resource links for which this is the primary resource link. $records = $DB->get_records($this->resourcelinktable, ['primaryresourcelinkid' => $resourcelinkid]); foreach ($records as $record) { $record->primaryresourcelinkid = null; $DB->update_record($this->resourcelinktable, $record); } // Delete resource link. $DB->delete_records($this->resourcelinktable, ['id' => $resourcelinkid]); $resourcelink->initialize(); return true; } /** * Get array of user objects. * * Obtain an array of User objects for users with a result sourcedId. The array may include users from other * resource links which are sharing this resource link. It may also be optionally indexed by the user ID of a specified scope. * * @param ResourceLink $resourcelink Resource link object * @param boolean $localonly True if only users within the resource link are to be returned * (excluding users sharing this resource link) * @param int $idscope Scope value to use for user IDs * @return array Array of User objects */ public function getUserResultSourcedIDsResourceLink($resourcelink, $localonly, $idscope) { global $DB; $users = []; $params = ['resourcelinkid' => $resourcelink->getRecordId()]; // Where clause for the subquery. $subwhere = "(id = :resourcelinkid AND primaryresourcelinkid IS NULL)"; if (!$localonly) { $subwhere .= " OR (primaryresourcelinkid = :resourcelinkid2 AND shareapproved = 1)"; $params['resourcelinkid2'] = $resourcelink->getRecordId(); } // The subquery. $subsql = "SELECT id FROM {{$this->resourcelinktable}} WHERE {$subwhere}"; // Our main where clause. $where = "resourcelinkid IN ($subsql)"; // Fields to be queried. $fields = 'id, ltiresultsourcedid, ltiuserkey, created, updated'; // Fetch records. $rs = $DB->get_recordset_select($this->userresulttable, $where, $params, '', $fields); foreach ($rs as $row) { $user = User::fromResourceLink($resourcelink, $row->ltiuserkey); $user->setRecordId($row->id); $user->ltiResultSourcedId = $row->ltiresultsourcedid; $user->created = $row->created; $user->updated = $row->updated; if (is_null($idscope)) { $users[] = $user; } else { $users[$user->getId($idscope)] = $user; } } $rs->close(); return $users; } /** * Get array of shares defined for this resource link. * * @param ResourceLink $resourcelink ResourceLink object * @return array Array of ResourceLinkShare objects */ public function getSharesResourceLink($resourcelink) { global $DB; $shares = []; $params = ['primaryresourcelinkid' => $resourcelink->getRecordId()]; $fields = 'id, shareapproved, consumerid'; $records = $DB->get_records($this->resourcelinktable, $params, 'consumerid', $fields); foreach ($records as $record) { $share = new ResourceLinkShare(); $share->resourceLinkId = $record->id; $share->approved = $record->shareapproved == 1; $shares[] = $share; } return $shares; } /* * ConsumerNonce methods */ /** * Load nonce object. * * @param ConsumerNonce $nonce Nonce object * @return boolean True if the nonce object was successfully loaded */ public function loadConsumerNonce($nonce) { global $DB; // Delete any expired nonce values. $now = time(); $DB->delete_records_select($this->noncetable, "expires <= ?", [$now]); // Load the nonce. $params = [ 'consumerid' => $nonce->getConsumer()->getRecordId(), 'value' => $nonce->getValue() ]; $result = $DB->get_field($this->noncetable, 'value', $params); return !empty($result); } /** * Save nonce object. * * @param ConsumerNonce $nonce Nonce object * @return boolean True if the nonce object was successfully saved */ public function saveConsumerNonce($nonce) { global $DB; $data = [ 'consumerid' => $nonce->getConsumer()->getRecordId(), 'value' => $nonce->getValue(), 'expires' => $nonce->expires ]; return $DB->insert_record($this->noncetable, (object) $data, false); } /* * ResourceLinkShareKey methods. */ /** * Load resource link share key object. * * @param ResourceLinkShareKey $sharekey ResourceLink share key object * @return boolean True if the resource link share key object was successfully loaded */ public function loadResourceLinkShareKey($sharekey) { global $DB; // Clear expired share keys. $now = time(); $where = "expires <= :expires"; $DB->delete_records_select($this->sharekeytable, $where, ['expires' => $now]); // Load share key. $fields = 'resourcelinkid, autoapprove, expires'; if ($sharekeyrecord = $DB->get_record($this->sharekeytable, ['sharekey' => $sharekey->getId()], $fields)) { if ($sharekeyrecord->resourcelinkid == $sharekey->resourceLinkId) { $sharekey->autoApprove = $sharekeyrecord->autoapprove == 1; $sharekey->expires = $sharekeyrecord->expires; return true; } } return false; } /** * Save resource link share key object. * * @param ResourceLinkShareKey $sharekey Resource link share key object * @return boolean True if the resource link share key object was successfully saved */ public function saveResourceLinkShareKey($sharekey) { global $DB; if ($sharekey->autoApprove) { $approve = 1; } else { $approve = 0; } $expires = $sharekey->expires; $params = [ 'sharekey' => $sharekey->getId(), 'resourcelinkid' => $sharekey->resourceLinkId, 'autoapprove' => $approve, 'expires' => $expires ]; return $DB->insert_record($this->sharekeytable, (object) $params, false); } /** * Delete resource link share key object. * * @param ResourceLinkShareKey $sharekey Resource link share key object * @return boolean True if the resource link share key object was successfully deleted */ public function deleteResourceLinkShareKey($sharekey) { global $DB; $DB->delete_records($this->sharekeytable, ['sharekey' => $sharekey->getId()]); $sharekey->initialize(); return true; } /* * User methods */ /** * Load user object. * * @param User $user User object * @return boolean True if the user object was successfully loaded */ public function loadUser($user) { global $DB; $userid = $user->getRecordId(); $fields = 'id, resourcelinkid, ltiuserkey, ltiresultsourcedid, created, updated'; if (!empty($userid)) { $row = $DB->get_record($this->userresulttable, ['id' => $userid], $fields); } else { $resourcelinkid = $user->getResourceLink()->getRecordId(); $userid = $user->getId(ToolProvider\ToolProvider::ID_SCOPE_ID_ONLY); $row = $DB->get_record_select( $this->userresulttable, "resourcelinkid = ? AND ltiuserkey = ?", [$resourcelinkid, $userid], $fields ); } if ($row) { $user->setRecordId($row->id); $user->setResourceLinkId($row->resourcelinkid); $user->ltiUserId = $row->ltiuserkey; $user->ltiResultSourcedId = $row->ltiresultsourcedid; $user->created = $row->created; $user->updated = $row->updated; return true; } return false; } /** * Save user object. * * @param User $user User object * @return boolean True if the user object was successfully saved */ public function saveUser($user) { global $DB; $now = time(); $isinsert = is_null($user->created); $user->updated = $now; $params = [ 'ltiresultsourcedid' => $user->ltiResultSourcedId, 'updated' => $user->updated ]; if ($isinsert) { $params['resourcelinkid'] = $user->getResourceLink()->getRecordId(); $params['ltiuserkey'] = $user->getId(ToolProvider\ToolProvider::ID_SCOPE_ID_ONLY); $user->created = $now; $params['created'] = $user->created; $id = $DB->insert_record($this->userresulttable, (object) $params); if ($id) { $user->setRecordId($id); return true; } } else { $params['id'] = $user->getRecordId(); return $DB->update_record($this->userresulttable, (object) $params); } return false; } /** * Delete user object. * * @param User $user User object * @return boolean True if the user object was successfully deleted */ public function deleteUser($user) { global $DB; $DB->delete_records($this->userresulttable, ['id' => $user->getRecordId()]); $user->initialize(); return true; } /** * Fetches the list of Context objects that are linked to a ToolConsumer. * * @param ToolConsumer $consumer * @return Context[] */ public function get_contexts_from_consumer(ToolConsumer $consumer) { global $DB; $contexts = []; $contextrecords = $DB->get_records($this->contexttable, ['consumerid' => $consumer->getRecordId()], '', 'lticontextkey'); foreach ($contextrecords as $record) { $context = Context::fromConsumer($consumer, $record->lticontextkey); $contexts[] = $context; } return $contexts; } /** * Fetches a resource link record that is associated with a ToolConsumer. * * @param ToolConsumer $consumer * @return ResourceLink */ public function get_resourcelink_from_consumer(ToolConsumer $consumer) { global $DB; $resourcelink = null; if ($resourcelinkrecord = $DB->get_record($this->resourcelinktable, ['consumerid' => $consumer->getRecordId()], 'ltiresourcelinkkey')) { $resourcelink = ResourceLink::fromConsumer($consumer, $resourcelinkrecord->ltiresourcelinkkey); } return $resourcelink; } /** * Fetches a resource link record that is associated with a Context object. * * @param Context $context * @return ResourceLink */ public function get_resourcelink_from_context(Context $context) { global $DB; $resourcelink = null; if ($resourcelinkrecord = $DB->get_record($this->resourcelinktable, ['contextid' => $context->getRecordId()], 'ltiresourcelinkkey')) { $resourcelink = ResourceLink::fromContext($context, $resourcelinkrecord->ltiresourcelinkkey); } return $resourcelink; } /** * Fetches the list of ToolConsumer objects that are linked to a tool. * * @param int $toolid * @return ToolConsumer[] */ public function get_consumers_mapped_to_tool($toolid) { global $DB; $consumers = []; $consumerrecords = $DB->get_records('enrol_lti_tool_consumer_map', ['toolid' => $toolid], '', 'consumerid'); foreach ($consumerrecords as $record) { $consumers[] = ToolConsumer::fromRecordId($record->consumerid, $this); } return $consumers; } /** * Builds a ToolConsumer object from a record object from the DB. * * @param stdClass $record The DB record object. * @param ToolConsumer $consumer */ protected function build_tool_consumer_object($record, ToolConsumer $consumer) { $consumer->setRecordId($record->id); $consumer->name = $record->name; $key = empty($record->consumerkey) ? $record->consumerkey256 : $record->consumerkey; $consumer->setKey($key); $consumer->secret = $record->secret; $consumer->ltiVersion = $record->ltiversion; $consumer->consumerName = $record->consumername; $consumer->consumerVersion = $record->consumerversion; $consumer->consumerGuid = $record->consumerguid; $consumer->profile = json_decode($record->profile ?? ''); $consumer->toolProxy = $record->toolproxy; $settings = unserialize($record->settings); if (!is_array($settings)) { $settings = array(); } $consumer->setSettings($settings); $consumer->protected = $record->protected == 1; $consumer->enabled = $record->enabled == 1; $consumer->enableFrom = null; if (!is_null($record->enablefrom)) { $consumer->enableFrom = $record->enablefrom; } $consumer->enableUntil = null; if (!is_null($record->enableuntil)) { $consumer->enableUntil = $record->enableuntil; } $consumer->lastAccess = null; if (!is_null($record->lastaccess)) { $consumer->lastAccess = $record->lastaccess; } $consumer->created = $record->created; $consumer->updated = $record->updated; } } classes/output/registration.php000064400000003615152266447110013001 0ustar00. /** * Tool registration page class. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti\output; defined('MOODLE_INTERNAL') || die; use renderable; use renderer_base; use templatable; use stdClass; /** * Tool registration page class. * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class registration implements renderable, templatable { /** @var returnurl The url to which the tool proxy should return */ protected $returnurl; /** * Construct a new tool registration page * @param string|null $returnurl The url the consumer wants us to return the user to (optional) */ public function __construct($returnurl = null) { $this->returnurl = $returnurl; } /** * Export the data. * * @param renderer_base $output * @return stdClass Data to be used for the template */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->returnurl = $this->returnurl; return $data; } } classes/task/sync_grades.php000064400000021756152266447110012200 0ustar00. /** * Handles synchronising grades for the enrolment LTI. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti\task; /** * Task for synchronising grades for the enrolment LTI. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_grades extends \core\task\scheduled_task { /** * Get a descriptive name for this task. * * @return string */ public function get_name() { return get_string('tasksyncgrades', 'enrol_lti'); } /** * Performs the synchronisation of grades. * * @return bool|void */ public function execute() { global $DB, $CFG; require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php'); require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuthBody.php'); require_once($CFG->dirroot . '/lib/completionlib.php'); require_once($CFG->libdir . '/gradelib.php'); require_once($CFG->dirroot . '/grade/querylib.php'); // Check if the authentication plugin is disabled. if (!is_enabled_auth('lti')) { mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti'))); return true; } // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong. if (!enrol_is_enabled('lti')) { mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti')); return true; } // Get all the enabled tools. if ($tools = \enrol_lti\helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'gradesync' => 1, 'ltiversion' => 'LTI-1p0/LTI-2p0'))) { foreach ($tools as $tool) { mtrace("Starting - Grade sync for shared tool '$tool->id' for the course '$tool->courseid'."); // Variables to keep track of information to display later. $usercount = 0; $sendcount = 0; // We check for all the users - users can access the same tool from different consumers. if ($ltiusers = $DB->get_records('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC')) { $completion = new \completion_info(get_course($tool->courseid)); foreach ($ltiusers as $ltiuser) { $mtracecontent = "for the user '$ltiuser->userid' in the tool '$tool->id' for the course " . "'$tool->courseid'"; $usercount = $usercount + 1; // Check if we do not have a serviceurl - this can happen if the sync process has an unexpected error. if (empty($ltiuser->serviceurl)) { mtrace("Skipping - Empty serviceurl $mtracecontent."); continue; } // Check if we do not have a sourceid - this can happen if the sync process has an unexpected error. if (empty($ltiuser->sourceid)) { mtrace("Skipping - Empty sourceid $mtracecontent."); continue; } // Need a valid context to continue. if (!$context = \context::instance_by_id($tool->contextid, IGNORE_MISSING)) { mtrace("Failed - Invalid contextid '$tool->contextid' for the tool '$tool->id'."); continue; } // Ok, let's get the grade. $grade = false; if ($context->contextlevel == CONTEXT_COURSE) { // Check if the user did not completed the course when it was required. if ($tool->gradesynccompletion && !$completion->is_course_complete($ltiuser->userid)) { mtrace("Skipping - Course not completed $mtracecontent."); continue; } // Get the grade. if ($grade = grade_get_course_grade($ltiuser->userid, $tool->courseid)) { $grademax = floatval($grade->item->grademax); $grade = $grade->grade; } } else if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); if ($tool->gradesynccompletion) { $data = $completion->get_data($cm, false, $ltiuser->userid); if ($data->completionstate != COMPLETION_COMPLETE_PASS && $data->completionstate != COMPLETION_COMPLETE) { mtrace("Skipping - Activity not completed $mtracecontent."); continue; } } $grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, $ltiuser->userid); if (!empty($grades->items[0]->grades)) { $grade = reset($grades->items[0]->grades); if (!empty($grade->item)) { $grademax = floatval($grade->item->grademax); } else { $grademax = floatval($grades->items[0]->grademax); } $grade = $grade->grade; } } if ($grade === false || $grade === null || strlen($grade) < 1) { mtrace("Skipping - Invalid grade $mtracecontent."); continue; } // No need to be dividing by zero. if (empty($grademax)) { mtrace("Skipping - Invalid grade $mtracecontent."); continue; } // Check to see if the grade has changed. if (!grade_floats_different($grade, $ltiuser->lastgrade)) { mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same."); continue; } // Sync with the external system. $floatgrade = $grade / $grademax; $body = \enrol_lti\helper::create_service_body($ltiuser->sourceid, $floatgrade); try { $response = sendOAuthBodyPOST('POST', $ltiuser->serviceurl, $ltiuser->consumerkey, $ltiuser->consumersecret, 'application/xml', $body); } catch (\Exception $e) { mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); mtrace($e->getMessage()); continue; } if (strpos(strtolower($response), 'success') !== false) { $DB->set_field('enrol_lti_users', 'lastgrade', grade_floatval($grade), array('id' => $ltiuser->id)); mtrace("Success - The grade '$floatgrade' $mtracecontent was sent."); $sendcount = $sendcount + 1; } else { mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send."); } } } mtrace("Completed - Synced grades for tool '$tool->id' in the course '$tool->courseid'. " . "Processed $usercount users; sent $sendcount grades."); mtrace(""); } } } } classes/task/sync_members.php000064400000046070152266447110012361 0ustar00. /** * Handles synchronising members using the enrolment LTI. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti\task; defined('MOODLE_INTERNAL') || die(); use core\task\scheduled_task; use core_user; use enrol_lti\data_connector; use enrol_lti\helper; use IMSGlobal\LTI\ToolProvider\Context; use IMSGlobal\LTI\ToolProvider\ResourceLink; use IMSGlobal\LTI\ToolProvider\ToolConsumer; use IMSGlobal\LTI\ToolProvider\User; use stdClass; require_once($CFG->dirroot . '/user/lib.php'); /** * Task for synchronising members using the enrolment LTI. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_members extends scheduled_task { /** @var array Array of user photos. */ protected $userphotos = []; /** @var data_connector $dataconnector A data_connector instance. */ protected $dataconnector; /** * Get a descriptive name for this task. * * @return string */ public function get_name() { return get_string('tasksyncmembers', 'enrol_lti'); } /** * Performs the synchronisation of members. */ public function execute() { if (!is_enabled_auth('lti')) { mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti'))); return; } // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong. if (!enrol_is_enabled('lti')) { mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti')); return; } $this->dataconnector = new data_connector(); // Get all the enabled tools. $tools = helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1, 'ltiversion' => 'LTI-1p0/LTI-2p0')); foreach ($tools as $tool) { mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'."); // Variables to keep track of information to display later. $usercount = 0; $enrolcount = 0; $unenrolcount = 0; // Fetch consumer records mapped to this tool. $consumers = $this->dataconnector->get_consumers_mapped_to_tool($tool->id); // Perform processing for each consumer. foreach ($consumers as $consumer) { mtrace("Requesting membership service for the tool consumer '{$consumer->getRecordId()}'"); // Get members through this tool consumer. $members = $this->fetch_members_from_consumer($consumer); // Check if we were able to fetch the members. if ($members === false) { mtrace("Skipping - Membership service request failed.\n"); continue; } // Fetched members count. $membercount = count($members); $usercount += $membercount; mtrace("$membercount members received.\n"); // Process member information. list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members); $enrolcount += $enrolledcount; // Now sync unenrolments for the consumer. $unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users); } mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " . "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.\n"); } // Sync the user profile photos. mtrace("Started - Syncing user profile images."); $countsyncedimages = $this->sync_profile_images(); mtrace("Completed - Synced $countsyncedimages profile images."); } /** * Fetches the members that belong to a ToolConsumer. * * @param ToolConsumer $consumer * @return bool|User[] */ protected function fetch_members_from_consumer(ToolConsumer $consumer) { $dataconnector = $this->dataconnector; // Get membership URL template from consumer profile data. $defaultmembershipsurl = null; if (isset($consumer->profile->service_offered)) { $servicesoffered = $consumer->profile->service_offered; foreach ($servicesoffered as $service) { if (isset($service->{'@id'}) && strpos($service->{'@id'}, 'tcp:ToolProxyBindingMemberships') !== false && isset($service->endpoint)) { $defaultmembershipsurl = $service->endpoint; if (isset($consumer->profile->product_instance->product_info->product_family->vendor->code)) { $vendorcode = $consumer->profile->product_instance->product_info->product_family->vendor->code; $defaultmembershipsurl = str_replace('{vendor_code}', $vendorcode, $defaultmembershipsurl); } $defaultmembershipsurl = str_replace('{product_code}', $consumer->getKey(), $defaultmembershipsurl); break; } } } $members = false; // Fetch the resource link linked to the consumer. $resourcelink = $dataconnector->get_resourcelink_from_consumer($consumer); if ($resourcelink !== null) { // Try to perform a membership service request using this resource link. $members = $this->do_resourcelink_membership_request($resourcelink); } // If membership service can't be performed through resource link, fallback through context memberships. if ($members === false) { // Fetch context records that are mapped to this ToolConsumer. $contexts = $dataconnector->get_contexts_from_consumer($consumer); // Perform membership service request for each of these contexts. foreach ($contexts as $context) { $contextmembership = $this->do_context_membership_request($context, $resourcelink, $defaultmembershipsurl); if ($contextmembership) { // Add $contextmembership contents to $members array. if (is_array($members)) { $members = array_merge($members, $contextmembership); } else { $members = $contextmembership; } } } } return $members; } /** * Method to determine whether to sync unenrolments or not. * * @param int $syncmode The tool's membersyncmode. * @return bool */ protected function should_sync_unenrol($syncmode) { return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_UNENROL_MISSING; } /** * Method to determine whether to sync enrolments or not. * * @param int $syncmode The tool's membersyncmode. * @return bool */ protected function should_sync_enrol($syncmode) { return $syncmode == helper::MEMBER_SYNC_ENROL_AND_UNENROL || $syncmode == helper::MEMBER_SYNC_ENROL_NEW; } /** * Performs synchronisation of member information and enrolments. * * @param stdClass $tool * @param ToolConsumer $consumer * @param User[] $members * @return array An array of users from processed members and the number that were enrolled. */ protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) { global $DB; $users = []; $enrolcount = 0; // Process member information. foreach ($members as $member) { // Set the user data. $user = new stdClass(); $user->username = helper::create_username($consumer->getKey(), $member->ltiUserId); $user->firstname = core_user::clean_field($member->firstname, 'firstname'); $user->lastname = core_user::clean_field($member->lastname, 'lastname'); $user->email = core_user::clean_field($member->email, 'email'); // Get the user data from the LTI consumer. $user = helper::assign_user_tool_data($tool, $user); $dbuser = core_user::get_user_by_username($user->username, 'id'); if ($dbuser) { // If email is empty remove it, so we don't update the user with an empty email. if (empty($user->email)) { unset($user->email); } $user->id = $dbuser->id; user_update_user($user); // Add the information to the necessary arrays. $users[$user->id] = $user; $this->userphotos[$user->id] = $member->image; } else { if ($this->should_sync_enrol($tool->membersyncmode)) { // If the email was stripped/not set then fill it with a default one. This // stops the user from being redirected to edit their profile page. if (empty($user->email)) { $user->email = $user->username . "@example.com"; } $user->auth = 'lti'; $user->id = user_create_user($user); // Add the information to the necessary arrays. $users[$user->id] = $user; $this->userphotos[$user->id] = $member->image; } } // Sync enrolments. if ($this->should_sync_enrol($tool->membersyncmode)) { // Enrol the user in the course. if (helper::enrol_user($tool, $user->id) === helper::ENROLMENT_SUCCESSFUL) { // Increment enrol count. $enrolcount++; } // Check if this user has already been registered in the enrol_lti_users table. if (!$DB->record_exists('enrol_lti_users', ['toolid' => $tool->id, 'userid' => $user->id])) { // Create an initial enrol_lti_user record that we can use later when syncing grades and members. $userlog = new stdClass(); $userlog->userid = $user->id; $userlog->toolid = $tool->id; $userlog->consumerkey = $consumer->getKey(); $DB->insert_record('enrol_lti_users', $userlog); } } } return [$users, $enrolcount]; } /** * Performs unenrolment of users that are no longer enrolled in the consumer side. * * @param stdClass $tool The tool record object. * @param string $consumerkey ensure we only unenrol users from this tool consumer. * @param array $currentusers The list of current users. * @return int The number of users that have been unenrolled. */ protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) { global $DB; $ltiplugin = enrol_get_plugin('lti'); if (!$this->should_sync_unenrol($tool->membersyncmode)) { return 0; } if (empty($currentusers)) { return 0; } $unenrolcount = 0; $select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey"; $ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey], 'lastaccess DESC', 'userid'); // Go through the users and check if any were never listed, if so, remove them. foreach ($ltiusersrs as $ltiuser) { if (!array_key_exists($ltiuser->userid, $currentusers)) { $instance = new stdClass(); $instance->id = $tool->enrolid; $instance->courseid = $tool->courseid; $instance->enrol = 'lti'; $ltiplugin->unenrol_user($instance, $ltiuser->userid); // Increment unenrol count. $unenrolcount++; } } $ltiusersrs->close(); return $unenrolcount; } /** * Performs synchronisation of user profile images. */ protected function sync_profile_images() { $counter = 0; foreach ($this->userphotos as $userid => $url) { if ($url) { $result = helper::update_user_profile_image($userid, $url); if ($result === helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) { $counter++; mtrace("Profile image succesfully downloaded and created for user '$userid' from $url."); } else { mtrace($result); } } } return $counter; } /** * Performs membership service request using an LTI Context object. * * If the context has a 'custom_context_memberships_url' setting, we use this to perform the membership service request. * Otherwise, if a context is associated with resource link, we try first to get the members using the * ResourceLink::doMembershipsService() method. * If we're still unable to fetch members from the resource link, we try to build a memberships URL from the memberships URL * endpoint template that is defined in the ToolConsumer profile and substitute the parameters accordingly. * * @param Context $context The context object. * @param ResourceLink $resourcelink The resource link object. * @param string $membershipsurltemplate The memberships endpoint URL template. * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. */ protected function do_context_membership_request(Context $context, ?ResourceLink $resourcelink = null, $membershipsurltemplate = '') { $dataconnector = $this->dataconnector; // Flag to indicate whether to save the context later. $contextupdated = false; // If membership URL is not set, try to generate using the default membership URL from the consumer profile. if (!$context->hasMembershipService()) { if (empty($membershipsurltemplate)) { mtrace("Skipping - No membership service available.\n"); return false; } if ($resourcelink === null) { $resourcelink = $dataconnector->get_resourcelink_from_context($context); } if ($resourcelink !== null) { // Try to perform a membership service request using this resource link. $resourcelinkmembers = $this->do_resourcelink_membership_request($resourcelink); if ($resourcelinkmembers) { // If we're able to fetch members using this resource link, return these. return $resourcelinkmembers; } } // If fetching memberships through resource link failed and we don't have a memberships URL, build one from template. mtrace("'custom_context_memberships_url' not set. Fetching default template: $membershipsurltemplate"); $membershipsurl = $membershipsurltemplate; // Check if we need to fetch tool code. $needstoolcode = strpos($membershipsurl, '{tool_code}') !== false; if ($needstoolcode) { $toolcode = false; // Fetch tool code from the resource link data. $lisresultsourcedidjson = $resourcelink->getSetting('lis_result_sourcedid'); if ($lisresultsourcedidjson) { $lisresultsourcedid = json_decode($lisresultsourcedidjson); if (isset($lisresultsourcedid->data->typeid)) { $toolcode = $lisresultsourcedid->data->typeid; } } if ($toolcode) { // Substitute fetched tool code value. $membershipsurl = str_replace('{tool_code}', $toolcode, $membershipsurl); } else { // We're unable to determine the tool code. End this processing. return false; } } // Get context_id parameter and substitute, if applicable. $membershipsurl = str_replace('{context_id}', $context->getId(), $membershipsurl); // Get context_type and substitute, if applicable. if (strpos($membershipsurl, '{context_type}') !== false) { $contexttype = $context->type !== null ? $context->type : 'CourseSection'; $membershipsurl = str_replace('{context_type}', $contexttype, $membershipsurl); } // Save this URL for the context's custom_context_memberships_url setting. $context->setSetting('custom_context_memberships_url', $membershipsurl); $contextupdated = true; } // Perform membership service request. $url = $context->getSetting('custom_context_memberships_url'); mtrace("Performing membership service request from context with URL {$url}."); $members = $context->getMembership(); // Save the context if membership request succeeded and if it has been updated. if ($members && $contextupdated) { $context->save(); } return $members; } /** * Performs membership service request using ResourceLink::doMembershipsService() method. * * @param ResourceLink $resourcelink * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. */ protected function do_resourcelink_membership_request(ResourceLink $resourcelink) { $members = false; $membershipsurl = $resourcelink->getSetting('ext_ims_lis_memberships_url'); $membershipsid = $resourcelink->getSetting('ext_ims_lis_memberships_id'); if ($membershipsurl && $membershipsid) { mtrace("Performing membership service request from resource link with membership URL: " . $membershipsurl); $members = $resourcelink->doMembershipsService(true); } return $members; } } templates/local/ltiadvantage/tool_details.mustache000064400000012032152266447110016522 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/tool_details Template which displays a list of tool endpoint URLs, allowing the values to be copied to the clipboard via js. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * dynamic_registration_info * dynamic_registration_url * manual_registration_info * manual_registration_urls Example context (json): { "dynamic_registration_info": "Help text describing dynamic registration...", "dynamic_registration_url": { "id": "ab34dgdfa", "name": "Dynamic registration URL", "url": "https://example.com/enrol/lti/register.php?tok=asdb123b12babsdb" }, "manual_registration_info": "Help text describing manual registration...", "manual_registration_urls": [ { "id": "asdb1234m", "name": "Authentication Request URL", "url": "https://example.com/enrol/lti/login.php" }, { "id": "123GHn123", "name": "JWKS URL", "url": "https://example.com/enrol/lti/jwks.php" } ] } }}
{{#str}}registrationdynamic, enrol_lti{{/str}}
{{{dynamic_registration_info}}}
{{#dynamic_registration_url}}
{{/dynamic_registration_url}}
{{#str}}registrationmanual, enrol_lti{{/str}}
{{manual_registration_info}}
{{#manual_registration_urls}} {{/manual_registration_urls}}
{{#str}}name, core{{/str}} {{#str}}url, core{{/str}}
{{#js}} require(['enrol_lti/tool_endpoints'], function(ToolEndpoints) { ToolEndpoints.init(); }); {{/js}} templates/local/ltiadvantage/cookies_required_notice.mustache000064400000002761152266447110020745 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/cookies_required_notice Displays a notice, reporting that cookies are required but couldn't be set. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * heading * notification Example context (json): { "heading": "Cookies are required", "notification": { "message": "You appear to be using an unsupported browser...", "extraclasses": "", "announce": true, "closebutton": false, "issuccess": false, "isinfo": false, "iswarning": true, "iserror": false } } }}

{{heading}}

{{#notification}} {{> core/notification}} {{/notification}} templates/local/ltiadvantage/platform_details.mustache000064400000004217152266447110017377 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/platform_details Template which displays a table containing platform configuration details. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * platform_details_info * platform_details * edit_platform_details_url Example context (json): { "platform_details_info": "help text explaining where to get these details from, etc", "platform_details": [ { "name": "name", "value": "Moodle LMS" }, { "name": "Platform ID", "value": "https://lms.example.com/" }, { "name": "Client ID", "value": "ab46f8ea123" } ], "edit_platform_details_url": "https://SITE/enrol/lti/register_platform.php?action=edit&id=x" } }}
{{{platform_details_info}}}
{{#platform_details}} {{/platform_details}}
{{name}} {{#value}}{{.}}{{/value}} {{^value}}-{{/value}}
{{#str}}editplatformdetails, enrol_lti{{/str}} templates/local/ltiadvantage/content_select.mustache000064400000010625152266447110017057 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/content_select Template which displays a list of published courses and activities. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * hascontent * action * launchid * sesskey * courses Example context (json): { "hascontent": true, "action": "https://example.com/enrol/lti/configure.php", "launchid": "123-456-789", "sesskey": "123456789", "courses": [ { "id": 24, "fullname": "Introduction to chemistry", "shared_course": false, "modules": [ { "id": 45, "name": "Quiz 1", "lineitem": true } ] }, { "id": 67, "fullname": "Programming 101", "shared_course": true } ] } }}

{{#str}}publishedcontent, enrol_lti{{/str}}

{{#hascontent}}
{{#courses}} {{#modules}} {{#lineitem}} {{/lineitem}} {{^lineitem}} {{/lineitem}} {{/modules}} {{/courses}}
{{#str}}addtocourse, enrol_lti{{/str}} {{#str}}addtogradebook, enrol_lti{{/str}}
{{fullname}} {{#shared_course}} {{/shared_course}} {{#shared_course}} {{/shared_course}}
{{name}} -
{{/hascontent}} {{^hascontent}} {{#str}}nopublishedcontent, enrol_lti{{/str}} {{/hascontent}}
{{#js}} require(['enrol_lti/content_select'], function(ContentSelect) { ContentSelect.init(); }); {{/js}} templates/local/ltiadvantage/deployments.mustache000064400000005462152266447110016414 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/deployments Template which displays a table view of all deployments attached to a registration, with options to delete and add. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * deployments_info * has_deployments * add_deployment_url Optional context variables for this template: * tool_deployments Example context (json): { "deployments_info": "A deployment ID will be generated when...", "has_deployments": true, "tool_deployments": [ { "name": "Site level deployment of tool x in platform y", "deploymentid": "deploy-id-12345", "deleteurl": "https://example.org/enrol/lti/manage_deployments.php?action=delete&id=2®istrationid=1" } ], "add_deployment_url": "https://example.org/enrol/lti/manage_deployments.php?action=add" } }}
{{{deployments_info}}}
{{#has_deployments}} {{#tool_deployments}} {{/tool_deployments}}
{{#str}}name, core{{/str}} {{#str}}deploymentid, enrol_lti{{/str}} {{#str}}actions, core{{/str}}
{{name}} {{deploymentid}} {{#pix}} t/delete, core, {{#str}} delete {{/str}}{{/pix}}
{{/has_deployments}} {{^has_deployments}}
{{#str}}nodeployments, enrol_lti{{/str}}

{{/has_deployments}}
templates/local/ltiadvantage/registered_platforms.mustache000064400000010315152266447110020266 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/registered_platforms Template which displays a table view of all registered apps with options to add, delete and manage deployments. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * hasregs * addurl Optional context variables for this template: * registrations Example context (json): { "hasregs": true, "registrations": [ { "name": "Platform Y registration", "hasdeployments": true, "countdeployments": 2, "editurl": "https://example.org/enrol/lti/register_platform.php?action=edit®id=4", "deploymentsurl": "https://example.org/enrol/lti/manage_deployments.php?registrationid=1", "deleteurl": "https://example.org/enrol/lti/register_platform.php?action=delete®id=4" } ], "addurl": "https://example.org/enrol/lti/manage_deployments.php?action=add" } }}
{{#hasregs}} {{#registrations}} {{/registrations}}
{{#str}}name, core{{/str}} {{#str}}details, enrol_lti{{/str}} {{#str}}deployments, enrol_lti{{/str}} {{#str}}registrationstatus, enrol_lti{{/str}} {{#str}}actions, core{{/str}}
{{name}} {{#str}}registerplatform:platformid, enrol_lti{{/str}}: {{issuer}}
{{#str}}registerplatform:clientid, enrol_lti{{/str}}: {{clientid}}
{{#hasdeployments}} {{countdeployments}} {{/hasdeployments}} {{^hasdeployments}} {{countdeployments}} {{/hasdeployments}} {{#isactive}} {{{statusstring}}} {{/isactive}} {{^isactive}} {{{statusstring}}} {{/isactive}} {{#pix}} t/viewdetails, core, {{#str}} viewtoolendpoints, enrol_lti {{/str}}{{/pix}} {{#pix}} platformdetails, enrol_lti, {{#str}} viewplatformdetails, enrol_lti {{/str}}{{/pix}} {{#pix}} managedeployments, enrol_lti, {{#str}} managedeployments, enrol_lti {{/str}}{{/pix}} {{#pix}} t/delete, core, {{#str}} delete {{/str}}{{/pix}}
{{/hasregs}} {{^hasregs}}
{{#str}}noregisteredplatforms, enrol_lti{{/str}}

{{/hasregs}}
templates/local/ltiadvantage/registration_view.mustache000064400000012711152266447110017610 0ustar00{{! 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 . }} {{! @template enrol_lti/local/ltiadvantage/registration_view Template which displays details about a registration, allowing users to view the dynamic registration URL, the manual registration URLs and tool deployments. All the URLs support copy to clipboard, included in the enrol_lti/local/ltiadvantage/tool_endpoints.mustache file. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * dynamic_registration_info * dynamic_registration_url * manual_registration_info * manual_registration_urls * platform_details_info * platform_details * edit_platform_details_url * deployments_info * has_deployments Optional context variables for this template: * tool_deployments Example context (json): { "dynamic_registration_info": "Help/information about dynamic registration", "dynamic_registration_url": { "id": "ab34dgdfa", "name": "Dynamic registration URL", "url": "https://example.com/enrol/lti/register.php?tok=asdb123b12babsdb" }, "manual_registration_info": "Help/information about manual registration", "manual_registration_urls": [ { "id": "asdb1234m", "name": "Authentication Request URL", "url": "https://example.com/enrol/lti/login.php?id=a2c94" }, { "id": "123GHn123", "name": "JWKS URL", "url": "https://example.com/enrol/lti/jwks.php" } ], "platform_details_info": "Once the tool has been set up in the platform, details from the platform must...", "platform_details": { "name": "My LMS", "platformid": "https://lms.example.com", "clientid": "a265bcd3a8f5bcd", "authenticationrequesturl": "https://lms.example.com/auth", "jwksurl": "https://lms.example.com/jwks", "accesstokenurl": "https://lms.example.com/token" }, "edit_platform_details_url": "https://SITE/enrol/lti/register_platform.php?action=edit®id=xx", "deployments_info": "A deployment ID will be generated when...", "has_deployments": true, "tool_deployments": [ { "name": "Site level deployment of tool x in platform y", "deploymentid": "deploy-id-12345", "deleteurl": "https://example.org/enrol/lti/manage_deployments.php?action=delete&id=2®istrationid=1" } ] } }} {{#str}}back, core{{/str}}
{{> enrol_lti/local/ltiadvantage/tool_details}}
{{> enrol_lti/local/ltiadvantage/platform_details}}
{{> enrol_lti/local/ltiadvantage/deployments}}
templates/proxy_registration.mustache000064400000002263152266447110014263 0ustar00{{! 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 . }} {{! @template enrol_lti/proxy_registration The content to display when editing a tool. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * returnurl The url to return the page to. Can be null if no return url avaliable. Example context (json): {} }} {{#str}} successfulregistration, enrol_lti {{/str}}
{{#returnurl}} {{#str}} continue {{/str}} {{/returnurl}} templates/copy_grid.mustache000064400000003262152266447110012267 0ustar00{{! 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 . }} {{! @template enrol_lti/copy_grid The content to display when editing a tool. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * rows An array of objects with label, hidelabel, text and id Example context (json): { "rows": [ { "label": "Tool URL", "text": "http://example.com/", "id": "toolurl", "hidelabel": false }, { "label": "Secret", "text": "ABCDEF1234567890", "id": "secret", "hidelabel": true } ] } }} {{#rows}}
{{> core/copy_box }}
{{/rows}} cartridge.php000064400000004254152266447110007236 0ustar00. /** * Generates an XML IMS Cartridge with the details for the given tool * * @package enrol_lti * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->dirroot . '/lib/weblib.php'); $toolid = null; $token = null; $filearguments = get_file_argument(); $arguments = explode('/', trim($filearguments, '/')); if (count($arguments) >= 2) { // Can put cartridge.xml at the end, or anything really. list($toolid, $token) = $arguments; } $toolid = optional_param('id', $toolid, PARAM_INT); $token = optional_param('token', $token, PARAM_ALPHANUM); // Only show the cartridge if the token parameter is correct. // If we do not compare with a shared secret, someone could very easily // guess an id for the enrolment. if (!\enrol_lti\helper::verify_cartridge_token($toolid, $token)) { throw new \moodle_exception('incorrecttoken', 'enrol_lti'); } $tool = \enrol_lti\helper::get_lti_tool($toolid); if (!is_enabled_auth('lti')) { throw new \moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti')); } else if (!enrol_is_enabled('lti')) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); } else if ($tool->status != ENROL_INSTANCE_ENABLED) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); } else { header('Content-Type: text/xml; charset=utf-8'); echo \enrol_lti\helper::create_cartridge($toolid); } launch_deeplink.php000064400000010121152266447110010405 0ustar00. /** * Handles LTI 1.3 deep linking launches. * * There are 2 pathways through this page: * 1. When first making a deep linking launch from the platform. The launch data is cached at this point, pending user * authentication, and the page is set such that the post-authentication redirect will return here. * 2. The post-authentication redirect. The launch data is fetched from the session launch cache, and the resource * selection view is rendered. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\http_client; use enrol_lti\local\ltiadvantage\lib\lti_cookie; use enrol_lti\local\ltiadvantage\lib\issuer_database; use enrol_lti\local\ltiadvantage\lib\launch_cache_session; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\published_resource_repository; use Packback\Lti1p3\LtiMessageLaunch; use Packback\Lti1p3\LtiServiceConnector; require_once(__DIR__ . '/../../config.php'); global $OUTPUT, $PAGE, $CFG; require_once($CFG->libdir . '/filelib.php'); $idtoken = optional_param('id_token', null, PARAM_RAW); $launchid = optional_param('launchid', null, PARAM_RAW); if (!is_enabled_auth('lti')) { throw new moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti')); } if (!enrol_is_enabled('lti')) { throw new moodle_exception('enrolisdisabled', 'enrol_lti'); } if (empty($idtoken) && empty($launchid)) { throw new coding_exception('Error: launch requires id_token'); } // First launch from the platform: get launch data and cache it in case the user's not authenticated. $sesscache = new launch_cache_session(); $issdb = new issuer_database(new application_registration_repository(), new deployment_repository()); $cookie = new lti_cookie(); $serviceconnector = new LtiServiceConnector($sesscache, new http_client()); if ($idtoken) { $messagelaunch = LtiMessageLaunch::new($issdb, $sesscache, $cookie, $serviceconnector) ->initialize($_POST); } if ($launchid) { $messagelaunch = LtiMessageLaunch::fromCache($launchid, $issdb, $sesscache, $cookie, $serviceconnector); } if (empty($messagelaunch)) { throw new moodle_exception('Bad launch. Deep linking launch data could not be found'); } // Authenticate the instructor. // Deep linking cannot use resource-specific provisioning modes, so it just uses a sensible 'existing accounts only' mode. $auth = get_auth_plugin('lti'); $auth->complete_login( $messagelaunch->getLaunchData(), new moodle_url('/enrol/lti/launch_deeplink.php', ['launchid' => $messagelaunch->getLaunchId()]), auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY ); require_login(null, false); global $USER, $CFG; $PAGE->set_context(context_system::instance()); $url = new moodle_url('/enrol/lti/launch_deeplink.php'); $PAGE->set_url($url); $PAGE->set_pagelayout('popup'); $PAGE->set_title(get_string('opentool', 'enrol_lti')); // Get all the published_resource view objects and render them for selection. global $USER; $resourcerepo = new published_resource_repository(); $resources = $resourcerepo->find_all_for_user($USER->id); $renderer = $PAGE->get_renderer('enrol_lti'); echo $OUTPUT->header(); echo $renderer->render_published_resource_selection_view($messagelaunch, $resources); echo $OUTPUT->footer(); amd/src/tool_endpoints.js000064400000003364152266447110011510 0ustar00// 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 . /** * Module supporting the dynamic and manual registration URLs in the tool registration admin setting. * * @module enrol_lti/tool_endpoints * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import 'core/copy_to_clipboard'; /** * DOM Selectors. * @type {{URL_VALUE: string}} */ const SELECTORS = { URL_VALUE: '[id^="lti_tool_endpoint_url_"]', }; /** * Focus handler for the registration URL field, enabling auto select of text on click. * * @param {Event} event a click event. */ const focusURLHandler = (event) => { const triggerElement = event.target.closest(SELECTORS.URL_VALUE); if (triggerElement === null) { return; } event.preventDefault(); triggerElement.select(); }; /** * Initialise the tool registration page, attaching handlers, etc. */ export const init = () => { // Event delegation supporting the select on focus behaviour (with text selection permitted on subsequent clicks). document.addEventListener('focusin', focusURLHandler); }; amd/src/content_select.js000064400000003756152266447110011466 0ustar00// 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 . /** * Module providing checkbox autoselection behaviour to the table on the select content deep linking view, launch_deeplink.php. * * @module enrol_lti/content_select * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Register the checkbox change events allowing the automatic selection/deselection of the // 'add to gradebook' and 'add to course' checkboxes when selecting an activity/resource. const registerEventHandlers = () => { document.addEventListener('change', e => { if (e.target.matches("input[type='checkbox'][name^='modules']")) { const value = e.target.value; const gradecheckbox = document.querySelector("input[type='checkbox'][name^='grades'][value='" + value + "']"); if (gradecheckbox) { gradecheckbox.checked = e.target.checked; } } if (e.target.matches("input[type='checkbox'][name^='grades']")) { const value = e.target.value; const modcheckbox = document.querySelector("input[type='checkbox'][name^='modules'][value='" + value + "']"); if (e.target.checked) { modcheckbox.checked = true; } } }); }; export const init = () => { registerEventHandlers(); }; amd/build/content_select.min.js000064400000001335152266447110012547 0ustar00define("enrol_lti/content_select",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{document.addEventListener("change",(e=>{if(e.target.matches("input[type='checkbox'][name^='modules']")){const value=e.target.value,gradecheckbox=document.querySelector("input[type='checkbox'][name^='grades'][value='"+value+"']");gradecheckbox&&(gradecheckbox.checked=e.target.checked)}if(e.target.matches("input[type='checkbox'][name^='grades']")){const value=e.target.value,modcheckbox=document.querySelector("input[type='checkbox'][name^='modules'][value='"+value+"']");e.target.checked&&(modcheckbox.checked=!0)}}))}})); //# sourceMappingURL=content_select.min.js.mapamd/build/tool_endpoints.min.js000064400000001474152266447110012602 0ustar00define("enrol_lti/tool_endpoints",["exports","core/copy_to_clipboard"],(function(_exports,_copy_to_clipboard){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Module supporting the dynamic and manual registration URLs in the tool registration admin setting. * * @module enrol_lti/tool_endpoints * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const SELECTORS_URL_VALUE='[id^="lti_tool_endpoint_url_"]',focusURLHandler=event=>{const triggerElement=event.target.closest(SELECTORS_URL_VALUE);null!==triggerElement&&(event.preventDefault(),triggerElement.select())};_exports.init=()=>{document.addEventListener("focusin",focusURLHandler)}})); //# sourceMappingURL=tool_endpoints.min.js.mapamd/build/tool_endpoints.min.js.map000064400000004270152266447110013353 0ustar00{"version":3,"file":"tool_endpoints.min.js","sources":["../src/tool_endpoints.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * Module supporting the dynamic and manual registration URLs in the tool registration admin setting.\n *\n * @module enrol_lti/tool_endpoints\n * @copyright 2021 Jake Dallimore \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport 'core/copy_to_clipboard';\n\n/**\n * DOM Selectors.\n * @type {{URL_VALUE: string}}\n */\nconst SELECTORS = {\n URL_VALUE: '[id^=\"lti_tool_endpoint_url_\"]',\n};\n\n/**\n * Focus handler for the registration URL field, enabling auto select of text on click.\n *\n * @param {Event} event a click event.\n */\nconst focusURLHandler = (event) => {\n const triggerElement = event.target.closest(SELECTORS.URL_VALUE);\n if (triggerElement === null) {\n return;\n }\n event.preventDefault();\n\n triggerElement.select();\n};\n\n/**\n * Initialise the tool registration page, attaching handlers, etc.\n */\nexport const init = () => {\n // Event delegation supporting the select on focus behaviour (with text selection permitted on subsequent clicks).\n document.addEventListener('focusin', focusURLHandler);\n};\n"],"names":["SELECTORS","focusURLHandler","event","triggerElement","target","closest","preventDefault","select","document","addEventListener"],"mappings":";;;;;;;;MA2BMA,oBACS,iCAQTC,gBAAmBC,cACfC,eAAiBD,MAAME,OAAOC,QAAQL,qBACrB,OAAnBG,iBAGJD,MAAMI,iBAENH,eAAeI,yBAMC,KAEhBC,SAASC,iBAAiB,UAAWR"}amd/build/content_select.min.js.map000064400000005105152266447110013322 0ustar00{"version":3,"file":"content_select.min.js","sources":["../src/content_select.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * Module providing checkbox autoselection behaviour to the table on the select content deep linking view, launch_deeplink.php.\n *\n * @module enrol_lti/content_select\n * @copyright 2021 Jake Dallimore \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n// Register the checkbox change events allowing the automatic selection/deselection of the\n// 'add to gradebook' and 'add to course' checkboxes when selecting an activity/resource.\nconst registerEventHandlers = () => {\n document.addEventListener('change', e => {\n if (e.target.matches(\"input[type='checkbox'][name^='modules']\")) {\n const value = e.target.value;\n const gradecheckbox = document.querySelector(\"input[type='checkbox'][name^='grades'][value='\" + value + \"']\");\n if (gradecheckbox) {\n gradecheckbox.checked = e.target.checked;\n }\n }\n\n if (e.target.matches(\"input[type='checkbox'][name^='grades']\")) {\n const value = e.target.value;\n const modcheckbox = document.querySelector(\"input[type='checkbox'][name^='modules'][value='\" + value + \"']\");\n if (e.target.checked) {\n modcheckbox.checked = true;\n }\n }\n });\n};\n\nexport const init = () => {\n registerEventHandlers();\n};\n"],"names":["document","addEventListener","e","target","matches","value","gradecheckbox","querySelector","checked","modcheckbox"],"mappings":"6JA2CoB,KAnBhBA,SAASC,iBAAiB,UAAUC,OAC5BA,EAAEC,OAAOC,QAAQ,2CAA4C,OACvDC,MAAQH,EAAEC,OAAOE,MACjBC,cAAgBN,SAASO,cAAc,iDAAmDF,MAAQ,MACpGC,gBACAA,cAAcE,QAAUN,EAAEC,OAAOK,YAIrCN,EAAEC,OAAOC,QAAQ,0CAA2C,OACtDC,MAAQH,EAAEC,OAAOE,MACjBI,YAAcT,SAASO,cAAc,kDAAoDF,MAAQ,MACnGH,EAAEC,OAAOK,UACTC,YAAYD,SAAU"}tests/helper_test.php000064400000046564152266447110010764 0ustar00. namespace enrol_lti; /** * Test the helper functionality. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class helper_test extends \advanced_testcase { /** * @var \stdClass $user1 A user. */ public $user1; /** * @var \stdClass $user2 A user. */ public $user2; /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Set this user as the admin. $this->setAdminUser(); // Get some of the information we need. $this->user1 = self::getDataGenerator()->create_user(); $this->user2 = self::getDataGenerator()->create_user(); } /** * Test the update user profile image function. */ public function test_update_user_profile_image(): void { global $DB, $CFG; // Set the profile image. \enrol_lti\helper::update_user_profile_image($this->user1->id, $this->getExternalTestFileUrl('/test.jpg')); // Get the new user record. $this->user1 = $DB->get_record('user', array('id' => $this->user1->id)); // Set the page details. $page = new \moodle_page(); $page->set_url('/user/profile.php'); $page->set_context(\context_system::instance()); $renderer = $page->get_renderer('core'); $usercontext = \context_user::instance($this->user1->id); // Get the user's profile picture and make sure it is correct. $userpicture = new \user_picture($this->user1); $this->assertSame($CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev=' .$this->user1->picture, $userpicture->get_url($page, $renderer)->out(false)); } /** * Test that we can not enrol past the maximum number of users allowed. */ public function test_enrol_user_max_enrolled(): void { global $DB; // Set up the LTI enrolment tool. $data = new \stdClass(); $data->maxenrolled = 1; $tool = $this->getDataGenerator()->create_lti_tool($data); // Now get all the information we need. $tool = \enrol_lti\helper::get_lti_tool($tool->id); // Enrol a user. $result = \enrol_lti\helper::enrol_user($tool, $this->user1->id); // Check that the user was enrolled. $this->assertEquals(true, $result); $this->assertEquals(1, $DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid))); // Try and enrol another user - should not happen. $result = \enrol_lti\helper::enrol_user($tool, $this->user2->id); // Check that this user was not enrolled and we are told why. $this->assertEquals(\enrol_lti\helper::ENROLMENT_MAX_ENROLLED, $result); $this->assertEquals(1, $DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid))); } /** * Test that we can not enrol when the enrolment has not started. */ public function test_enrol_user_enrolment_not_started(): void { global $DB; // Set up the LTI enrolment tool. $data = new \stdClass(); $data->enrolstartdate = time() + DAYSECS; // Make sure it is in the future. $tool = $this->getDataGenerator()->create_lti_tool($data); // Now get all the information we need. $tool = \enrol_lti\helper::get_lti_tool($tool->id); // Try and enrol a user - should not happen. $result = \enrol_lti\helper::enrol_user($tool, $this->user1->id); // Check that this user was not enrolled and we are told why. $this->assertEquals(\enrol_lti\helper::ENROLMENT_NOT_STARTED, $result); $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid))); } /** * Test that we can not enrol when the enrolment has finished. */ public function test_enrol_user_enrolment_finished(): void { global $DB; // Set up the LTI enrolment tool. $data = new \stdClass(); $data->enrolenddate = time() - DAYSECS; // Make sure it is in the past. $tool = $this->getDataGenerator()->create_lti_tool($data); // Now get all the information we need. $tool = \enrol_lti\helper::get_lti_tool($tool->id); // Try and enrol a user - should not happen. $result = \enrol_lti\helper::enrol_user($tool, $this->user1->id); // Check that this user was not enrolled and we are told why. $this->assertEquals(\enrol_lti\helper::ENROLMENT_FINISHED, $result); $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid))); } /** * Test returning the number of available tools. */ public function test_count_lti_tools(): void { $generator = $this->getDataGenerator(); // Create two tools belonging to the same course. $course1 = $generator->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $generator->create_lti_tool($data); $generator->create_lti_tool($data); // Create two more tools in a separate course. $course2 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course2->id; $generator->create_lti_tool($data); // Set the next tool to disabled. $data->status = ENROL_INSTANCE_DISABLED; $generator->create_lti_tool($data); // Count all the tools. $count = \enrol_lti\helper::count_lti_tools(); $this->assertEquals(4, $count); // Count all the tools in course 1. $count = \enrol_lti\helper::count_lti_tools(array('courseid' => $course1->id)); $this->assertEquals(2, $count); // Count all the tools in course 2 that are disabled. $count = \enrol_lti\helper::count_lti_tools(array('courseid' => $course2->id, 'status' => ENROL_INSTANCE_DISABLED)); $this->assertEquals(1, $count); // Count all the tools that are enabled. $count = \enrol_lti\helper::count_lti_tools(array('status' => ENROL_INSTANCE_ENABLED)); $this->assertEquals(3, $count); } /** * Test returning the list of available tools. */ public function test_get_lti_tools(): void { $generator = $this->getDataGenerator(); // Create two tools belonging to the same course. $course1 = $generator->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $generator->create_lti_tool($data); $tool2 = $generator->create_lti_tool($data); // Create two more tools in a separate course. $course2 = $generator->create_course(); $data = new \stdClass(); $data->courseid = $course2->id; $tool3 = $generator->create_lti_tool($data); // Set the next tool to disabled. $data->status = ENROL_INSTANCE_DISABLED; $tool4 = $generator->create_lti_tool($data); // Get all the tools. $tools = \enrol_lti\helper::get_lti_tools(); // Check that we got all the tools. $this->assertEquals(4, count($tools)); // Get all the tools in course 1. $tools = \enrol_lti\helper::get_lti_tools(array('courseid' => $course1->id)); // Check that we got all the tools in course 1. $this->assertEquals(2, count($tools)); $this->assertTrue(isset($tools[$tool1->id])); $this->assertTrue(isset($tools[$tool2->id])); // Get all the tools in course 2 that are disabled. $tools = \enrol_lti\helper::get_lti_tools(array('courseid' => $course2->id, 'status' => ENROL_INSTANCE_DISABLED)); // Check that we got all the tools in course 2 that are disabled. $this->assertEquals(1, count($tools)); $this->assertTrue(isset($tools[$tool4->id])); // Get all the tools that are enabled. $tools = \enrol_lti\helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED)); // Check that we got all the tools that are enabled. $this->assertEquals(3, count($tools)); $this->assertTrue(isset($tools[$tool1->id])); $this->assertTrue(isset($tools[$tool2->id])); $this->assertTrue(isset($tools[$tool3->id])); } /** * Test getting the launch url of a tool. */ public function test_get_launch_url(): void { $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $id = $tool1->id; $launchurl = \enrol_lti\helper::get_launch_url($id); $this->assertEquals('https://www.example.com/moodle/enrol/lti/tool.php?id=' . $id, $launchurl->out()); } /** * Test getting the cartridge url of a tool. */ public function test_get_cartridge_url(): void { global $CFG; $slasharguments = $CFG->slasharguments; $CFG->slasharguments = false; $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $id = $tool1->id; $token = \enrol_lti\helper::generate_cartridge_token($id); $launchurl = \enrol_lti\helper::get_cartridge_url($tool1); $this->assertEquals('https://www.example.com/moodle/enrol/lti/cartridge.php?id=' . $id . '&token=' . $token, $launchurl->out()); $CFG->slasharguments = true; $launchurl = \enrol_lti\helper::get_cartridge_url($tool1); $this->assertEquals('https://www.example.com/moodle/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml', $launchurl->out()); $CFG->slasharguments = $slasharguments; } /** * Test getting the cartridge url of a tool. */ public function test_get_proxy_url(): void { global $CFG; $slasharguments = $CFG->slasharguments; $CFG->slasharguments = false; $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $id = $tool1->id; $token = \enrol_lti\helper::generate_proxy_token($id); $launchurl = \enrol_lti\helper::get_proxy_url($tool1); $this->assertEquals('https://www.example.com/moodle/enrol/lti/proxy.php?id=' . $id . '&token=' . $token, $launchurl->out()); $CFG->slasharguments = true; $launchurl = \enrol_lti\helper::get_proxy_url($tool1); $this->assertEquals('https://www.example.com/moodle/enrol/lti/proxy.php/' . $id . '/' . $token . '/', $launchurl->out()); $CFG->slasharguments = $slasharguments; } /** * Test getting the name of a tool. */ public function test_get_name(): void { $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $name = \enrol_lti\helper::get_name($tool1); $this->assertEquals('Course: Test course 1', $name); $tool1->name = 'Shared course'; $name = \enrol_lti\helper::get_name($tool1); $this->assertEquals('Shared course', $name); } /** * Test getting the description of a tool. */ public function test_get_description(): void { $generator = $this->getDataGenerator(); $course1 = $generator->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $generator->create_lti_tool($data); $description = \enrol_lti\helper::get_description($tool1); $this->assertStringContainsString('Test course 1 Lorem ipsum dolor sit amet', $description); $module1 = $generator->create_module('assign', array( 'course' => $course1->id )); $data = new \stdClass(); $data->cmid = $module1->cmid; $tool2 = $generator->create_lti_tool($data); $description = \enrol_lti\helper::get_description($tool2); $this->assertStringContainsString('Test assign 1', $description); } /** * Test getting the icon of a tool. */ public function test_get_icon(): void { global $CFG; $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool = $this->getDataGenerator()->create_lti_tool($data); $icon = \enrol_lti\helper::get_icon($tool); $icon = $icon->out(); // Only local icons are supported by the LTI framework. $this->assertStringContainsString($CFG->wwwroot, $icon); } /** * Test verifying a cartridge token. */ public function test_verify_cartridge_token(): void { $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $token = \enrol_lti\helper::generate_cartridge_token($tool1->id); $this->assertTrue(\enrol_lti\helper::verify_cartridge_token($tool1->id, $token)); $this->assertFalse(\enrol_lti\helper::verify_cartridge_token($tool1->id, 'incorrect token!')); } /** * Test verifying a proxy token. */ public function test_verify_proxy_token(): void { $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $token = \enrol_lti\helper::generate_proxy_token($tool1->id); $this->assertTrue(\enrol_lti\helper::verify_proxy_token($tool1->id, $token)); $this->assertFalse(\enrol_lti\helper::verify_proxy_token($tool1->id, 'incorrect token!')); } /** * Data provider for the set_xpath test. */ public static function set_xpath_provider(): array { return [ "Correct structure" => [ "parameters" => [ "/root" => [ "/firstnode" => "Content 1", "/parentnode" => [ "/childnode" => "Content 2" ] ] ], "expected" => "test_correct_xpath-expected.xml" ], "A null value, but no node to remove" => [ "parameters" => [ "/root" => [ "/nonexistant" => null, "/firstnode" => "Content 1" ] ], "expected" => "test_missing_node-expected.xml" ], "A string value, but no node existing to set" => [ "parameters" => [ "/root" => [ "/nonexistant" => "This will not be set", "/firstnode" => "Content 1" ] ], "expected" => "test_missing_node-expected.xml" ], "Array but no children exist" => [ "parameters" => [ "/root" => [ "/nonexistant" => [ "/alsononexistant" => "This will not be set" ], "/firstnode" => "Content 1" ] ], "expected" => "test_missing_node-expected.xml" ], "Remove nodes" => [ "parameters" => [ "/root" => [ "/parentnode" => [ "/childnode" => null ], "/firstnode" => null ] ], "expected" => "test_nodes_removed-expected.xml" ], "Get by attribute" => [ "parameters" => [ "/root" => [ "/ambiguous[@id='1']" => 'Content 1' ] ], "expected" => "test_ambiguous_nodes-expected.xml" ] ]; } /** * Test set_xpath. * @dataProvider set_xpath_provider * @param array $parameters A hash of parameters represented by a heirarchy of xpath expressions * @param string $expected The name of the fixture file containing the expected result. */ public function test_set_xpath($parameters, $expected): void { $helper = new \ReflectionClass('enrol_lti\\helper'); $function = $helper->getMethod('set_xpath'); $document = new \DOMDocument(); $document->load(realpath(self::get_fixture_path(__NAMESPACE__, 'input.xml'))); $xpath = new \DOMXpath($document); $function->invokeArgs(null, [$xpath, $parameters]); $result = $document->saveXML(); $expected = file_get_contents(realpath(self::get_fixture_path(__NAMESPACE__, $expected))); $this->assertEquals($expected, $result); } /** * Test set_xpath when an incorrect xpath expression is given. */ public function test_set_xpath_incorrect_xpath(): void { $parameters = [ "/root" => [ "/firstnode" => null, "/parentnode*&#^*#(" => [ "/childnode" => null ], ] ]; $helper = new \ReflectionClass('enrol_lti\\helper'); $function = $helper->getMethod('set_xpath'); $document = new \DOMDocument(); $document->load(realpath(self::get_fixture_path(__NAMESPACE__, 'input.xml'))); $xpath = new \DOMXpath($document); $this->expectException('coding_exception'); $function->invokeArgs(null, [$xpath, $parameters]); } /** * Test create cartridge. */ public function test_create_cartridge(): void { global $CFG; $course1 = $this->getDataGenerator()->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool1 = $this->getDataGenerator()->create_lti_tool($data); $cartridge = \enrol_lti\helper::create_cartridge($tool1->id); $this->assertStringContainsString('Test LTI', $cartridge); $this->assertStringContainsString("$CFG->wwwroot/theme/image.php/boost/theme/1/favicon", $cartridge); $this->assertStringContainsString("$CFG->wwwroot/enrol/lti/tool.php?id=$tool1->id", $cartridge); } } tests/fixtures/test_correct_xpath-expected.xml000064400000000327152266447110016016 0ustar00 Content 1 Content 2 tests/fixtures/test_nodes_removed-expected.xml000064400000000227152266447110016001 0ustar00 tests/fixtures/test_missing_node-expected.xml000064400000000303152266447110015621 0ustar00 Content 1 tests/fixtures/tool_consumer_profile.json000064400000001507152266447110015075 0ustar00{ "@context" : ["http://purl.imsglobal.org/ctx/lti/v2/Profile"], "@type" : "ToolConsumerProfile", "lti_version" : "LTI-2p0", "@id" : "http://alpha.university.edu/resource/ToolConsumerProfile/12434", "guid" : "b6ffa601-ce1d-4549-9ccf-145670a964d4", "product_instance" : { }, "service_offered" : [ { "@type" : "RestService", "@id" : "http://www.example.com/moodle/lti/resource/ToolConsumerProfile/12434#Result.item", "endpoint" : "http://www.example.com/moodle/enpoint.php", "format" : ["application/vnd.ims.lti.v2.toolproxy+json"], "action" : ["POST"] } ], "capability_offered" : [ "Person.name.given", "Person.name.family", "Person.email.primary", "User.image", "Result.sourcedId", "basic-lti-launch-request", "Result.autocreate", "Result.url" ] } tests/fixtures/input.xml000064400000000333152266447110011447 0ustar00 tests/fixtures/test_ambiguous_nodes-expected.xml000064400000000303152266447110016326 0ustar00 Content 1 tests/generator/behat_enrol_lti_generator.php000064400000003340152266447110015615 0ustar00. /** * Behat data generator for enrol_lti. * * @package enrol_lti * @category test * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_enrol_lti_generator extends behat_generator_base { /** * Get a list of the entities that Behat can create using the generator step. * * @return array the list of creatable entities. */ protected function get_creatable_entities(): array { return [ 'application registrations' => [ 'singular' => 'application registration', 'datagenerator' => 'application_registration', 'required' => ['name'] ], 'published resources' => [ 'singular' => 'published resource', 'datagenerator' => 'published_resource', 'required' => ['name', 'activity', 'course'], 'switchids' => ['activity' => 'activityid', 'course' => 'courseid'] ] ]; } } tests/local/ltiadvantage/lib/issuer_database_test.php000064400000011610152266447110017126 0ustar00. namespace enrol_lti\local\ltiadvantage\lib; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use Packback\Lti1p3\LtiDeployment; use Packback\Lti1p3\LtiRegistration; /** * Tests for the issuer_database class. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\lib\issuer_database */ final class issuer_database_test extends \advanced_testcase { /** * Test the Moodle implementation of the library database method test_find_registration_by_issuer(). * * @covers ::findRegistrationByIssuer */ public function test_find_registration_by_issuer(): void { $this->resetAfterTest(); $appregrepo = new application_registration_repository(); $appreg = application_registration::create( 'My platform', 'a2c94a2c94', new \moodle_url('https://lms.example.com'), 'client-id-123', new \moodle_url('https://lms.example.com/lti/auth'), new \moodle_url('https://lms.example.com/lti/jwks'), new \moodle_url('https://lms.example.com/lti/token') ); $appregrepo->save($appreg); $issuerdb = new issuer_database($appregrepo, new deployment_repository()); $registration = $issuerdb->findRegistrationByIssuer('https://lms.example.com', 'client-id-123'); $this->assertInstanceOf(LtiRegistration::class, $registration); $this->assertEquals($appreg->get_authenticationrequesturl()->out(false), $registration->getAuthLoginUrl()); $this->assertEquals($appreg->get_jwksurl()->out(false), $registration->getKeySetUrl()); $this->assertEquals($appreg->get_accesstokenurl()->out(false), $registration->getAuthTokenUrl()); $this->assertEquals($appreg->get_clientid(), $registration->getClientId()); $this->assertEquals($appreg->get_platformid()->out(false), $registration->getIssuer()); $this->assertNull($issuerdb->findRegistrationByIssuer('https://lms.example.com', 'client-id-456')); $this->expectException(\coding_exception::class); $this->expectExceptionMessageMatches('/The param \'clientid\' is required. /'); $issuerdb->findRegistrationByIssuer('https://lms.example.com'); } /** * Test the Moodle implementation of the library database method test_find_deployment(). * * @covers ::findDeployment */ public function test_find_deployment(): void { $this->resetAfterTest(); $appregrepo = new application_registration_repository(); $appreg = application_registration::create( 'My platform', 'a2c94a2c94', new \moodle_url('https://lms.example.com'), 'client-id-123', new \moodle_url('https://lms.example.com/lti/auth'), new \moodle_url('https://lms.example.com/lti/jwks'), new \moodle_url('https://lms.example.com/lti/token') ); $appreg = $appregrepo->save($appreg); $dep = $appreg->add_tool_deployment('Site wide tool deployment', 'deployment-id-1'); $deploymentrepo = new deployment_repository(); $deploymentrepo->save($dep); $issuerdb = new issuer_database($appregrepo, new deployment_repository()); $deployment = $issuerdb->findDeployment('https://lms.example.com', 'deployment-id-1', 'client-id-123'); $this->assertInstanceOf(LtiDeployment::class, $deployment); $this->assertEquals($dep->get_deploymentid(), $deployment->getDeploymentId()); $this->assertNull($issuerdb->findDeployment('https://lms.example.com', 'deployment-id-1', 'client-id-456')); $this->assertNull($issuerdb->findDeployment('https://lms.example.com', 'deployment-id-2', 'client-id-123')); $this->expectException(\coding_exception::class); $this->expectExceptionMessageMatches('/Both issuer and client id are required to identify platform registrations /'); $issuerdb->findDeployment('https://lms.example.com', 'deployment-id-2'); } } tests/local/ltiadvantage/lib/launch_cache_session_test.php000064400000006055152266447110020137 0ustar00. namespace enrol_lti\local\ltiadvantage\lib; /** * Tests for the launch_cache_session class. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\lib\launch_cache_session */ final class launch_cache_session_test extends \advanced_testcase { /** * Test that the session cache, and in particular distinct object instances, can cache and retrieve launch data. * * Using different objects simulates the kind of usage we expect: uses across different requests. * * @covers ::cacheLaunchData */ public function test_cache_launch_data(): void { $lcs = new launch_cache_session(); $lcs->cacheLaunchData('TestKey', ['JWT body' => 'xxx']); $lcs2 = new launch_cache_session(); $this->assertEquals(['JWT body' => 'xxx'], $lcs2->getLaunchData('TestKey')); $this->assertNull($lcs2->getLaunchData('TestKey123')); } /** * Test that the session cache, and in particular distinct object instances, can cache and check the nonce data. * * Using different objects simulates the kind of usage we expect: uses across different requests. * * @covers ::cacheNonce */ public function test_cache_and_check_nonce(): void { $lcs = new launch_cache_session(); $lcs->cacheNonce('my_nonce_123', 'my_state_234'); $lcs2 = new launch_cache_session(); $this->assertTrue($lcs2->checkNonceIsValid('my_nonce_123', 'my_state_234')); $this->assertFalse($lcs2->checkNonceIsValid('different_nonce', 'my_state_234')); $this->assertFalse($lcs2->checkNonceIsValid('my_nonce_123', 'different_state')); } /** * Test that the session cache, and in particular distinct object instances, can purge cached launch data. * * Using different objects simulates the kind of usage we expect: uses across different requests. * * @covers ::purge */ public function test_purge(): void { $lcs = new launch_cache_session(); $lcs->cacheLaunchData('TestKey', ['JWT body' => 'xxx']); $lcs2 = new launch_cache_session(); $this->assertEquals(['JWT body' => 'xxx'], $lcs2->getLaunchData('TestKey')); $lcs2->purge(); $this->assertNull($lcs2->getLaunchData('TestKey')); } } tests/local/ltiadvantage/lti_advantage_testcase.php000064400000053133152266447110016666 0ustar00. use enrol_lti\helper; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use enrol_lti\local\ltiadvantage\service\tool_launch_service; use Packback\Lti1p3\LtiMessageLaunch; /** * Parent class for LTI Advantage tests, providing environment setup and mock user launches. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class lti_advantage_testcase extends \advanced_testcase { /** @var string the default issuer for tests extending this class. */ protected $issuer = 'https://lms.example.org'; /** * Helper to return a user which has been bound to the LTI credentials provided and is deemed a valid linked user. * * @param string $sub the subject id string * @param array $migrationclaiminfo mocked migration claim information, allowing the mock auth to bind to an existing user. * @return stdClass the user record. */ protected function lti_advantage_user_authenticates(string $sub, array $migrationclaiminfo = []): \stdClass { $auth = get_auth_plugin('lti'); $mockjwt = [ 'iss' => $this->issuer, 'sub' => $sub, 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1222', // Must match deployment in create_test_env. 'aud' => '123', // Must match registration in create_test_environment. 'exp' => time() + 60, 'nonce' => 'some-nonce-value-123', 'given_name' => 'John', 'family_name' => 'Smith', 'email' => 'smithj@example.org' ]; if (!empty($migrationclaiminfo)) { if (isset($migrationclaiminfo['consumer_key'])) { $base = [ $migrationclaiminfo['consumer_key'], $mockjwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $mockjwt['iss'], $mockjwt['aud'], $mockjwt['exp'], $mockjwt['nonce'] ]; $basestring = implode('&', $base); $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 'oauth_consumer_key' => $migrationclaiminfo['consumer_key'], ]; if (isset($migrationclaiminfo['signing_secret'])) { $sig = base64_encode(hash_hmac('sha256', $basestring, $migrationclaiminfo['signing_secret'])); $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; } } $claimprops = ['user_id', 'context_id', 'tool_consumer_instance_guid', 'resource_link_id']; foreach ($claimprops as $prop) { if (!empty($migrationclaiminfo[$prop])) { $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'][$prop] = $migrationclaiminfo[$prop]; } } } $secrets = !empty($migrationclaiminfo['signing_secret']) ? [$migrationclaiminfo['signing_secret']] : []; return $auth->find_or_create_user_from_launch($mockjwt, $secrets); } /** * Get a list of users ready for use with mock launches by providing an array of user ids. * * @param array $ids the platform user_ids for the users. * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). * @param string $role the LTI role to include in the user data. * @return array the users list. */ protected static function get_mock_launch_users_with_ids( array $ids, bool $includepicture = false, string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ): array { $users = []; foreach ($ids as $id) { $user = [ 'user_id' => $id, 'given_name' => 'Firstname' . $id, 'family_name' => 'Surname' . $id, 'email' => "firstname.surname{$id}@lms.example.org", 'roles' => [$role] ]; if ($includepicture) { $user['picture'] = $this->getExternalTestFileUrl('/test.jpg'); } $users[] = $user; } return $users; } /** * Get a mock LtiMessageLaunch object, as if a user had launched from a resource link in the platform. * * @param \stdClass $resource the resource record, allowing the mock to generate a link to this. * @param array $mockuser the user on the platform who is performing the launch. * @param string|null $resourcelinkid the id of resource link in the platform, if desired. * @param array|null $ags array representing the lti-ags claim info. Pass null to omit, empty array to use a default. * @param bool $nrps whether to include a mock NRPS claim or not. * @param array|null $migrationclaiminfo contains consumer key, secret and any fields which are sent in the claim. * @param array|null $customparams an array of custom params to send, or null to just use defaults. * @param mixed $aud the array or string value of aud to use in the mock launch data. * @return LtiMessageLaunch the mock launch object with test launch data. */ protected function get_mock_launch(\stdClass $resource, array $mockuser, ?string $resourcelinkid = null, ?array $ags = [], bool $nrps = true, ?array $migrationclaiminfo = null, ?array $customparams = null, $aud = '123'): LtiMessageLaunch { $mocklaunch = $this->getMockBuilder(LtiMessageLaunch::class) ->onlyMethods(['getLaunchData', 'getLaunchId']) ->disableOriginalConstructor() ->getMock(); $mocklaunch->expects($this->any()) ->method('getLaunchData') ->will($this->returnCallback( function() use ($resource, $mockuser, $resourcelinkid, $migrationclaiminfo, $ags, $nrps, $customparams, $aud) { // This simulates the data in the jwt['body'] of a real resource link launch. // Real launches would of course have this data and authenticity of the user verified. $rltitle = $resourcelinkid ? "Resource link $resourcelinkid in platform" : "Resource link in platform"; $rlid = $resourcelinkid ?: '12345'; $data = [ 'iss' => $this->issuer, // Must match registration in create_test_environment. 'aud' => $aud, // Must match registration in create_test_environment. 'sub' => $mockuser['user_id'], // User id on the platform site. 'exp' => time() + 60, 'nonce' => 'some-nonce-value-123', 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1', // Must match registration. 'https://purl.imsglobal.org/spec/lti/claim/roles' => $mockuser['roles'] ?? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'], 'https://purl.imsglobal.org/spec/lti/claim/resource_link' => [ 'title' => $rltitle, 'id' => $rlid, // Arbitrary, will be mapped to the user during resource link launch. ], "https://purl.imsglobal.org/spec/lti/claim/context" => [ "id" => "context-id-12345", "label" => "ITS 123", "title" => "ITS 123 Machine Learning", "type" => ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"] ], 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri' => 'https://this-moodle-tool.example.org/context/24/resource/14', 'given_name' => $mockuser['given_name'], 'family_name' => $mockuser['family_name'], 'email' => $mockuser['email'], ]; if (!is_null($customparams)) { $data['https://purl.imsglobal.org/spec/lti/claim/custom'] = $customparams; } else { $data['https://purl.imsglobal.org/spec/lti/claim/custom'] = [ 'id' => $resource->uuid, ]; } if (is_array($ags)) { if (empty($ags)) { $agsclaim = [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ]; } else { $agsclaim = $ags; } $data["https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"] = $agsclaim; } if ($nrps) { $data['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] = [ 'context_memberships_url' => 'https://lms.example.org/context/24/memberships', 'service_versions' => ['2.0'] ]; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if ($migrationclaiminfo) { if (isset($migrationclaiminfo['consumer_key'])) { $base = [ $migrationclaiminfo['consumer_key'], $data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $data['iss'], $data['aud'], $data['exp'], $data['nonce'] ]; $basestring = implode('&', $base); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 'oauth_consumer_key' => $migrationclaiminfo['consumer_key'], ]; if (isset($migrationclaiminfo['signing_secret'])) { $sig = base64_encode(hash_hmac('sha256', $basestring, $migrationclaiminfo['signing_secret'])); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; } } $claimprops = ['user_id', 'context_id', 'tool_consumer_instance_guid', 'resource_link_id']; foreach ($claimprops as $prop) { if (!empty($migrationclaiminfo[$prop])) { $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'][$prop] = $migrationclaiminfo[$prop]; } } } return $data; } )); $mocklaunch->expects($this->any()) ->method('getLaunchId') ->will($this->returnCallback(function() { return uniqid('lti1p3_launch_', true); })); return $mocklaunch; } /** * Sets up and returns a test course, including LTI-published resources, ready for testing. * * @param bool $enableauthplugin whether to enable the auth plugin during setup. * @param bool $enableenrolplugin whether to enable the enrol plugin during setup. * @param bool $membersync whether or not the published resource support membership sync with the platform. * @param int $membersyncmode the mode of member sync to set up on the shared resource. * @param bool $gradesync whether or not to enabled gradesync on the published resources. * @param bool $gradesynccompletion whether or not to require gradesynccompletion on the published resources. * @param int $enrolstartdate the unix time when the enrolment starts, or 0 for no start time. * @param int $provisioningmodeinstructor the teacher provisioning mode for all created resources, 0 for default (prompt). * @param int $provisioningmodelearner the student provisioning mode for all created resources, 0 for default (auto). * @return array array of objects for use in individual tests; courses, tools. */ protected function create_test_environment(bool $enableauthplugin = true, bool $enableenrolplugin = true, bool $membersync = true, int $membersyncmode = helper::MEMBER_SYNC_ENROL_AND_UNENROL, bool $gradesync = true, bool $gradesynccompletion = false, int $enrolstartdate = 0, int $provisioningmodeinstructor = 0, int $provisioningmodelearner = 0): array { global $CFG; require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->dirroot . '/auth/lti/auth.php'); if ($enableauthplugin) { $this->enable_auth(); } if ($enableenrolplugin) { $this->enable_enrol(); } // Set up the registration and deployment. $reg = application_registration::create( 'Example LMS application', 'a2c94a2c94', new moodle_url($this->issuer), '123', new moodle_url('https://example.org/authrequesturl'), new moodle_url('https://example.org/jwksurl'), new moodle_url('https://example.org/accesstokenurl') ); $regrepo = new application_registration_repository(); $reg = $regrepo->save($reg); $deployment = $reg->add_tool_deployment('My tool deployment', '1'); $deploymentrepo = new deployment_repository(); $deployment = $deploymentrepo->save($deployment); $generator = $this->getDataGenerator(); $course = $generator->create_course(['enablecompletion' => 1]); // Create a module and publish it. $mod = $generator->create_module('assign', ['course' => $course->id, 'grade' => 100, 'completionsubmit' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC]); $tooldata = [ 'cmid' => $mod->cmid, 'courseid' => $course->id, 'membersyncmode' => $membersyncmode, 'membersync' => $membersync, 'gradesync' => $gradesync, 'gradesynccompletion' => $gradesynccompletion, 'ltiversion' => 'LTI-1p3', 'enrolstartdate' => $enrolstartdate, 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY ]; $tool = $generator->create_lti_tool((object)$tooldata); $tool = helper::get_lti_tool($tool->id); // Create a second module and publish it. $mod = $generator->create_module('assign', ['course' => $course->id, 'grade' => 100, 'completionsubmit' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC]); $tooldata = [ 'cmid' => $mod->cmid, 'courseid' => $course->id, 'membersyncmode' => $membersyncmode, 'membersync' => $membersync, 'gradesync' => $gradesync, 'gradesynccompletion' => $gradesynccompletion, 'ltiversion' => 'LTI-1p3', 'enrolstartdate' => $enrolstartdate, 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY ]; $tool2 = $generator->create_lti_tool((object)$tooldata); $tool2 = helper::get_lti_tool($tool2->id); // Create a course and publish it. $tooldata = [ 'courseid' => $course->id, 'membersyncmode' => $membersyncmode, 'membersync' => $membersync, 'gradesync' => $gradesync, 'gradesynccompletion' => $gradesynccompletion, 'ltiversion' => 'LTI-1p3', 'enrolstartdate' => $enrolstartdate, 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY ]; $tool3 = $generator->create_lti_tool((object)$tooldata); $tool3 = helper::get_lti_tool($tool3->id); return [$course, $tool, $tool2, $tool3, $reg, $deployment]; } /** * Enable auth_lti plugin. */ protected function enable_auth() { $class = \core_plugin_manager::resolve_plugininfo_class('auth'); $class::enable_plugin('lti', true); } /** * Enable enrol_lti plugin. */ protected function enable_enrol() { $class = \core_plugin_manager::resolve_plugininfo_class('enrol'); $class::enable_plugin('lti', true); } /** * Helper to get a tool_launch_service instance. * * @return tool_launch_service the instance. */ protected function get_tool_launch_service(): tool_launch_service { return new tool_launch_service( new deployment_repository(), new application_registration_repository(), new resource_link_repository(), new user_repository(), new context_repository() ); } /** * Set up data representing a several published legacy tools, including tool records, tool consumer maps and a user. * * @param stdClass $course the course in which to create the tools. * @param array $legacydata array containing user id, consumer key and tool secrets for creation of records. * @return array array containing [tool1record, tool2record, consumerrecord, userrecord]. */ protected function setup_legacy_data(\stdClass $course, array $legacydata): array { // Legacy data: create a consumer record. global $DB; $generator = $this->getDataGenerator(); $now = time(); $consumerrecord = (object) [ 'name' => 'consumer name', 'consumerkey256' => $legacydata['consumer_key'], 'secret' => '0987654321fff', 'protected' => true, 'enabled' => true, 'created' => $now, 'updated' => $now, ]; $consumerrecord->id = $DB->insert_record('enrol_lti_lti2_consumer', $consumerrecord); // Legacy data: create some modules and publish them as tools, using different secrets, over LTI 1.1. $tools = []; $toolconsumermaprecords = []; foreach ($legacydata['tools'] as $tool) { $mod = $generator->create_module('assign', ['course' => $course->id]); $tooldata = [ 'cmid' => $mod->cmid, 'courseid' => $course->id, 'membersyncmode' => helper::MEMBER_SYNC_ENROL_AND_UNENROL, 'membersync' => false, 'ltiversion' => 'LTI-1p0/LTI-2p0', 'secret' => $tool['secret'] ]; $legacytool = $generator->create_lti_tool((object)$tooldata); $tools[] = $legacytool; $toolconsumermaprecords[] = ['toolid' => $legacytool->id, 'consumerid' => $consumerrecord->id]; } // Legacy data: create the tool consumer map, which is created during launches. $DB->insert_records('enrol_lti_tool_consumer_map', $toolconsumermaprecords); // Legacy data: create the user who launched the tools over LTI 1.1. if (!empty($legacydata['users'])) { $legacyusers = []; foreach ($legacydata['users'] as $legacyuser) { $legacyusers[] = $generator->create_user([ 'username' => helper::create_username($consumerrecord->consumerkey256, $legacyuser['user_id']), 'auth' => 'lti', ]); } } return [$tools, $consumerrecord, $legacyusers ?? null]; } } tests/local/ltiadvantage/viewobject/published_resource_test.php000064400000004003152266447110021247 0ustar00. namespace enrol_lti\local\ltiadvantage\viewobject; /** * Tests for published_resource view objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\viewobject\published_resource */ final class published_resource_test extends \advanced_testcase { /** * Test creating a simple published_resource view object and fetching information about it. * * @covers ::__construct */ public function test_create(): void { $pr = new published_resource( 'Assignment one', 'Course 1', 23, 45, 2, 'uuid-123-abc', true, 110.50, false ); $this->assertEquals('Assignment one', $pr->get_name()); $this->assertEquals('Course 1', $pr->get_coursefullname()); $this->assertEquals(23, $pr->get_courseid()); $this->assertEquals(45, $pr->get_contextid()); $this->assertEquals(2, $pr->get_id()); $this->assertEquals('uuid-123-abc', $pr->get_uuid()); $this->assertEquals(true, $pr->supports_grades()); $this->assertEquals(110.50, $pr->get_grademax()); $this->assertEquals(false, $pr->is_course()); } } tests/local/ltiadvantage/entity/deployment_test.php000064400000020054152266447110016720 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for deployment. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\deployment */ final class deployment_test extends \advanced_testcase { /** * Test creation of the object instances. * * @dataProvider instantiation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_creation(array $args, array $expectations): void { if (!$expectations['valid']) { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); deployment::create(...array_values($args)); } else { $deployment = deployment::create(...array_values($args)); $this->assertEquals($args['deploymentname'], $deployment->get_deploymentname()); $this->assertEquals($args['deploymentid'], $deployment->get_deploymentid()); $this->assertEquals($args['registrationid'], $deployment->get_registrationid()); $this->assertEquals($args['legacyconsumerkey'], $deployment->get_legacy_consumer_key()); $this->assertEquals($args['id'], $deployment->get_id()); } } /** * Data provider for testing object instantiation. * @return array the data for testing. */ public static function instantiation_data_provider(): array { return [ 'Valid deployment creation, no id or legacy consumer key' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => 'deployment-abcde', 'deploymentname' => 'Global platform deployment', 'id' => null, 'legacyconsumerkey' => null, ], 'expectations' => [ 'valid' => true, ] ], 'Valid deployment creation, with id, no legacy consumer key' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => 'deployment-abcde', 'deploymentname' => 'Global platform deployment', 'id' => 45, 'legacyconsumerkey' => null, ], 'expectations' => [ 'valid' => true, ] ], 'Valid deployment creation, with id and legacy consumer key' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => 'deployment-abcde', 'deploymentname' => 'Global platform deployment', 'id' => 45, 'legacyconsumerkey' => '1bcb23dfff', ], 'expectations' => [ 'valid' => true, ] ], 'Invalid deployment creation, invalid id' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => 'deployment-abcde', 'deploymentname' => 'Global platform deployment', 'id' => 0, 'legacyconsumerkey' => null, ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'id must be a positive int' ] ], 'Invalid deployment creation, empty deploymentname' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => 'deployment-abcde', 'deploymentname' => '', 'id' => null, 'legacyconsumerkey' => null, ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'deploymentname' arg. Cannot be an empty string." ] ], 'Invalid deployment creation, empty deploymentid' => [ 'args' => [ 'registrationid' => 99, 'deploymentid' => '', 'deploymentname' => 'Global platform deployment', 'id' => null, 'legacyconsumerkey' => null, ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'deploymentid' arg. Cannot be an empty string." ] ] ]; } /** * Test verifying that a context can only be created from a deployment that has an id. * * @covers ::add_context */ public function test_add_context(): void { $deploymentwithid = deployment::create(123, 'deploymentid123', 'Global tool deployment', 55); $context = $deploymentwithid->add_context('context-id-123', ['CourseSection']); $this->assertInstanceOf(context::class, $context); $this->assertEquals(55, $context->get_deploymentid()); $deploymentwithoutid = deployment::create(123, 'deploymentid123', 'Global tool deployment'); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Can't add context to a deployment that hasn't first been saved"); $deploymentwithoutid->add_context('context-id-345', ['Group']); } /** * Test verifying that a resource_link can only be created from a deployment that has an id. * * @covers ::add_resource_link */ public function test_add_resource_link(): void { $deploymentwithid = deployment::create(123, 'deploymentid123', 'Global tool deployment', 55); $resourcelink = $deploymentwithid->add_resource_link('res-link-id-123', 45); $this->assertInstanceOf(resource_link::class, $resourcelink); $this->assertEquals(55, $resourcelink->get_deploymentid()); $resourcelink2 = $deploymentwithid->add_resource_link('res-link-id-456', 45, 66); $this->assertEquals(66, $resourcelink2->get_contextid()); $deploymentwithoutid = deployment::create(123, 'deploymentid123', 'Global tool deployment'); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Can't add resource_link to a deployment that hasn't first been saved"); $deploymentwithoutid->add_resource_link('res-link-id-123', 45); } /** * Test the setter set_legacy_consumer_key. * * @covers ::set_legacy_consumer_key */ public function test_set_legacy_consumer_key(): void { $deployment = deployment::create(12, 'deploy-id-123', 'Global tool deployment'); $deployment->set_legacy_consumer_key(str_repeat('a', 255)); $this->assertEquals(str_repeat('a', 255), $deployment->get_legacy_consumer_key()); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Legacy consumer key too long. Cannot exceed 255 chars.'); $deployment->set_legacy_consumer_key(str_repeat('a', 256)); } } tests/local/ltiadvantage/entity/nrps_info_test.php000064400000011523152266447110016536 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for nrps_info. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\nrps_info */ final class nrps_info_test extends \advanced_testcase { /** * Test creation of the object instances. * * @dataProvider instantiation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_create(array $args, array $expectations): void { if (!$expectations['valid']) { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); nrps_info::create(...array_values($args)); } else { $nrpsinfo = nrps_info::create(...array_values($args)); $this->assertEquals($args['contextmembershipsurl'], $nrpsinfo->get_context_memberships_url()); $this->assertEquals($expectations['serviceversions'], $nrpsinfo->get_service_versions()); $this->assertEquals('https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', $nrpsinfo->get_service_scope()); } } /** * Data provider for testing object instantiation. * @return array the data for testing. */ public static function instantiation_data_provider(): array { return [ 'Valid creation' => [ 'args' => [ 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), 'serviceversions' => ['1.0', '2.0'], ], 'expectations' => [ 'valid' => true, 'serviceversions' => ['1.0', '2.0'] ] ], 'Missing service version' => [ 'args' => [ 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), 'serviceversions' => [], ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Service versions array cannot be empty' ] ], 'Invalid service version' => [ 'args' => [ 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), 'serviceversions' => ['1.1'], ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid Names and Roles service version '1.1'" ] ], 'Duplicate service version' => [ 'args' => [ 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), 'serviceversions' => ['1.0', '1.0'], ], 'expectations' => [ 'valid' => true, 'serviceversions' => ['1.0'] ] ] ]; } /** * Verify that the contextmembershipurl property can be gotten and is immutable. * * @covers ::get_context_memberships_url */ public function test_get_context_memberships_url(): void { $nrpsendpoint = 'https://lms.example.com/45/memberships'; $nrpsinfo = nrps_info::create(new \moodle_url($nrpsendpoint)); $membershipsurlcopy = $nrpsinfo->get_context_memberships_url(); $this->assertEquals($nrpsendpoint, $membershipsurlcopy->out(false)); $rlid = '01234567-1234-5678-90ab-123456789abc'; $membershipsurlcopy->param('rlid', $rlid); $this->assertEquals($nrpsendpoint . '?rlid=' . $rlid, $membershipsurlcopy->out(false)); $this->assertEquals($nrpsendpoint, $nrpsinfo->get_context_memberships_url()->out(false)); } } tests/local/ltiadvantage/entity/user_test.php000064400000070026152266447110015522 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for user. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\user */ final class user_test extends \advanced_testcase { /** * Test creation of a user instance using the factory method. * * @dataProvider create_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_create(array $args, array $expectations): void { if ($expectations['valid']) { $user = user::create(...array_values($args)); $this->assertInstanceOf(user::class, $user); $this->assertEquals($expectations['id'], $user->get_id()); $this->assertEquals($expectations['localid'], $user->get_localid()); $this->assertEquals($expectations['resourcelinkid'], $user->get_resourcelinkid()); $this->assertEquals($expectations['resourceid'], $user->get_resourceid()); $this->assertEquals($expectations['deploymentid'], $user->get_deploymentid()); $this->assertEquals($expectations['sourceid'], $user->get_sourceid()); $this->assertEquals($expectations['lang'], $user->get_lang()); $this->assertEquals($expectations['timezone'], $user->get_timezone()); $this->assertEquals($expectations['city'], $user->get_city()); $this->assertEquals($expectations['country'], $user->get_country()); $this->assertEquals($expectations['institution'], $user->get_institution()); $this->assertEquals($expectations['maildisplay'], $user->get_maildisplay()); $this->assertEquals($expectations['lastgrade'], $user->get_lastgrade()); $this->assertEquals($expectations['lastaccess'], $user->get_lastaccess()); } else { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); user::create(...array_values($args)); } } /** * Data provider for testing the user::create() method. * * @return array the data for testing. */ public static function create_data_provider(): array { global $CFG; return [ 'Valid create, only required args provided' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99' ], 'expectations' => [ 'valid' => true, 'resourceid' => 22, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => '', 'country' => '', 'institution' => '', 'maildisplay' => 2, 'lastgrade' => 0.0, 'lastaccess' => null, 'id' => null, 'localid' => 2, 'resourcelinkid' => null, ] ], 'Valid create, all args provided explicitly' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'My institution', 'maildisplay' => 1, 'lastgrade' => 50.55, 'lastaccess' => 14567888, 'resourcelinkid' => 44, 'id' => 22 ], 'expectations' => [ 'valid' => true, 'resourceid' => 22, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'My institution', 'maildisplay' => 1, 'lastgrade' => 50.55, 'lastaccess' => 14567888, 'resourcelinkid' => 44, 'localid' => 2, 'id' => 22, ] ], 'Valid create, optional args explicitly nulled for default values' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'My institution', 'maildisplay' => null, 'lastgrade' => null, 'lastaccess' => null, 'resourcelinkid' => null, 'id' => null ], 'expectations' => [ 'valid' => true, 'resourceid' => 22, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'My institution', 'maildisplay' => 2, 'lastgrade' => 0.0, 'lastaccess' => null, 'resourcelinkid' => null, 'localid' => 2, 'id' => null ] ], 'Invalid create, lang with bad value (vvvv not installed)' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => 'vvvv', 'timezone' => '99', ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid lang 'vvvv' provided." ] ], 'Invalid create, timezone with bad value' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => 'NOT/FOUND', ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." ] ], 'Invalid create, explicitly provided country with bad value' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => '', 'country' => 'FFF', ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid country code 'FFF'." ] ], 'Invalid create, explicit maildisplay with bad value' => [ 'args' => [ 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => '', 'country' => '', 'institution' => '', 'maildisplay' => 3, ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid maildisplay value '3'. Must be in the range {0..2}." ] ], ]; } /** * Test creation of a user instance from a resource link. * * @dataProvider create_from_resource_link_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create_from_resource_link */ public function test_create_from_resource_link(array $args, array $expectations): void { if ($expectations['valid']) { $user = user::create_from_resource_link(...array_values($args)); $this->assertInstanceOf(user::class, $user); $this->assertEquals($expectations['id'], $user->get_id()); $this->assertEquals($expectations['localid'], $user->get_localid()); $this->assertEquals($expectations['resourcelinkid'], $user->get_resourcelinkid()); $this->assertEquals($expectations['resourceid'], $user->get_resourceid()); $this->assertEquals($expectations['deploymentid'], $user->get_deploymentid()); $this->assertEquals($expectations['sourceid'], $user->get_sourceid()); $this->assertEquals($expectations['lang'], $user->get_lang()); $this->assertEquals($expectations['city'], $user->get_city()); $this->assertEquals($expectations['country'], $user->get_country()); $this->assertEquals($expectations['institution'], $user->get_institution()); $this->assertEquals($expectations['timezone'], $user->get_timezone()); $this->assertEquals($expectations['maildisplay'], $user->get_maildisplay()); $this->assertEquals($expectations['lastgrade'], $user->get_lastgrade()); $this->assertEquals($expectations['lastaccess'], $user->get_lastaccess()); } else { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); user::create_from_resource_link(...array_values($args)); } } /** * Data provider used in testing the user::create_from_resource_link() method. * * @return array the data for testing. */ public static function create_from_resource_link_data_provider(): array { global $CFG; return [ 'Valid creation, all args provided explicitly' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'platform', 'maildisplay' => 1 ], 'expectations' => [ 'valid' => true, 'id' => null, 'localid' => 2, 'resourcelinkid' => 11, 'resourceid' => 22, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'platform', 'maildisplay' => 1, 'lastgrade' => 0.0, 'lastaccess' => null ] ], 'Valid creation, only required args provided, explicit values' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => 'UTC' ], 'expectations' => [ 'valid' => true, 'id' => null, 'localid' => 2, 'resourcelinkid' => 11, 'resourceid' => 22, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => 'UTC', 'city' => '', 'country' => '', 'institution' => '', 'maildisplay' => 2, 'lastgrade' => 0.0, 'lastaccess' => null ] ], 'Invalid creation, only required args provided, empty sourceid' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'user' => 2, 'deploymentid' => 33, 'sourceid' => '', 'lang' => $CFG->lang, 'timezone' => 'UTC' ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Invalid sourceid value. Cannot be an empty string.' ] ], 'Invalid creation, only required args provided, empty lang' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'user' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => '', 'timezone' => 'UTC' ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Invalid lang value. Cannot be an empty string.' ] ], 'Invalid creation, only required args provided, empty timezone' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '' ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Invalid timezone value. Cannot be an empty string.' ] ], 'Invalid creation, only required args provided, invalid lang (vvvv not installed)' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => 'vvvv', 'timezone' => 'UTC' ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid lang 'vvvv' provided." ] ], 'Invalid creation, only required args provided, invalid timezone' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => 'NOT/FOUND' ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." ] ], 'Invalid creation, all args provided explicitly, invalid country' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'FFF', 'institution' => 'platform', 'maildisplay' => 1 ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid country code 'FFF'." ] ], 'Invalid creation, all args provided explicitly, invalid maildisplay' => [ 'args' => [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => '99', 'city' => 'Melbourne', 'country' => 'AU', 'institution' => 'platform', 'maildisplay' => 4 ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid maildisplay value '4'. Must be in the range {0..2}." ] ], ]; } /** * Helper to create a simple, working user for testing. * * @return user a user instance. */ protected function create_test_user(): user { global $CFG; $args = [ 'resourcelinkid' => 11, 'resourceid' => 22, 'userid' => 2, 'deploymentid' => 33, 'sourceid' => 'user-id-123', 'lang' => $CFG->lang, 'timezone' => 'UTC' ]; return user::create_from_resource_link(...array_values($args)); } /** * Test the behaviour of the user setters and getters. * * @dataProvider setters_getters_data_provider * @param string $methodname the name of the setter * @param mixed $arg the argument to the setter * @param array $expectations the array of expectations * @covers ::__construct */ public function test_setters_and_getters(string $methodname, $arg, array $expectations): void { $user = $this->create_test_user(); $setter = 'set_'.$methodname; $getter = 'get_'.$methodname; if ($expectations['valid']) { $user->$setter($arg); if (isset($expectations['expectedvalue'])) { $this->assertEquals($expectations['expectedvalue'], $user->$getter()); } else { $this->assertEquals($arg, $user->$getter()); } } else { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); $user->$setter($arg); } } /** * Data provider for testing the user object setters. * * @return array the array of test data. */ public static function setters_getters_data_provider(): array { global $CFG; return [ 'Testing set_resourcelinkid with valid id' => [ 'methodname' => 'resourcelinkid', 'arg' => 8, 'expectations' => [ 'valid' => true, ] ], 'Testing set_resourcelinkid with invalid id' => [ 'methodname' => 'resourcelinkid', 'arg' => -1, 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid resourcelinkid '-1' provided. Must be > 0." ] ], 'Testing set_city with a non-empty string' => [ 'methodname' => 'city', 'arg' => 'Melbourne', 'expectations' => [ 'valid' => true, ] ], 'Testing set_city with an empty string' => [ 'methodname' => 'city', 'arg' => '', 'expectations' => [ 'valid' => true, ] ], 'Testing set_country with a valid country code' => [ 'methodname' => 'country', 'arg' => 'AU', 'expectations' => [ 'valid' => true, ] ], 'Testing set_country with an empty string' => [ 'methodname' => 'country', 'arg' => '', 'expectations' => [ 'valid' => true, ] ], 'Testing set_country with an invalid country code' => [ 'methodname' => 'country', 'arg' => 'FFF', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid country code 'FFF'." ] ], 'Testing set_institution with a non-empty string' => [ 'methodname' => 'institution', 'arg' => 'Some institution', 'expectations' => [ 'valid' => true, ] ], 'Testing set_institution with an empty string' => [ 'methodname' => 'institution', 'arg' => '', 'expectations' => [ 'valid' => true, ] ], 'Testing set_timezone with a valid real timezone' => [ 'methodname' => 'timezone', 'arg' => 'Pacific/Wallis', 'expectations' => [ 'valid' => true, ] ], 'Testing set_timezone with a valid server timezone value' => [ 'methodname' => 'timezone', 'arg' => '99', 'expectations' => [ 'valid' => true, ] ], 'Testing set_timezone with an invalid timezone value' => [ 'methodname' => 'timezone', 'arg' => 'NOT/FOUND', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." ] ], 'Testing set_maildisplay with a valid int: 0' => [ 'methodname' => 'maildisplay', 'arg' => '0', 'expectations' => [ 'valid' => true, ] ], 'Testing set_maildisplay with a valid int: 1' => [ 'methodname' => 'maildisplay', 'arg' => '1', 'expectations' => [ 'valid' => true, ] ], 'Testing set_maildisplay with a valid int: 2' => [ 'methodname' => 'maildisplay', 'arg' => '2', 'expectations' => [ 'valid' => true, ] ], 'Testing set_maildisplay with invalid int: -1' => [ 'methodname' => 'maildisplay', 'arg' => '-1', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid maildisplay value '-1'. Must be in the range {0..2}." ] ], 'Testing set_maildisplay with invalid int: 3' => [ 'methodname' => 'maildisplay', 'arg' => '3', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid maildisplay value '3'. Must be in the range {0..2}." ] ], 'Testing set_lang with valid lang code' => [ 'methodname' => 'lang', 'arg' => $CFG->lang, 'expectations' => [ 'valid' => true, ] ], 'Testing set_lang with an empty string' => [ 'methodname' => 'lang', 'arg' => '', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Invalid lang value. Cannot be an empty string.' ] ], 'Testing set_lang with invalid lang code' => [ 'methodname' => 'lang', 'arg' => 'ff', 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid lang 'ff' provided." ] ], 'Testing set_lastgrade with valid grade' => [ 'methodname' => 'lastgrade', 'arg' => 0.0, 'expectations' => [ 'valid' => true ] ], 'Testing set_lastgrade with valid non zero grade' => [ 'methodname' => 'lastgrade', 'arg' => 150.0, 'expectations' => [ 'valid' => true ] ], 'Testing set_lastgrade with valid non zero long decimal grade' => [ 'methodname' => 'lastgrade', 'arg' => 150.777779, 'expectations' => [ 'valid' => true, 'expectedvalue' => 150.77778 ] ], 'Testing set_lastaccess with valid time' => [ 'methodname' => 'lastaccess', 'arg' => 4, 'expectations' => [ 'valid' => true ] ], 'Testing set_lastaccess with invalid time' => [ 'methodname' => 'lastaccess', 'arg' => -1, 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Cannot set negative access time' ] ], ]; } } tests/local/ltiadvantage/entity/migration_claim_test.php000064400000021700152266447110017675 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; /** * Tests for migration_claim. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\migration_claim */ final class migration_claim_test extends \advanced_testcase { /** * Setup run for each test case. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Returns a stub legacy_consumer_repository, allowing tests to verify claims using a predefined secret. */ protected function get_stub_legacy_consumer_repo() { $mockedlegacyconsumerrepo = $this->createStub(legacy_consumer_repository::class); $mockedlegacyconsumerrepo->method('get_consumer_secrets') ->willReturn(['consumer_secret']); return $mockedlegacyconsumerrepo; } /** * Test instantiation and getters of the migration_claim. * * @dataProvider migration_claim_provider * @param array $lti1p1migrationclaim the lti1p1 migration claim. * @param string $deploymentid string id of the tool deployment. * @param string $platform string url of the issuer. * @param string $clientid string id of the client. * @param string $exp expiry time. * @param string $nonce nonce. * @param bool $stublegacyconsumerrepo Whether the legacy consumer repo is a stub * @param array $expected array containing expectation data. * @covers ::__construct */ public function test_migration_claim( array $lti1p1migrationclaim, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce, bool $stublegacyconsumerrepo, array $expected, ): void { if ($stublegacyconsumerrepo) { $legacyconsumerrepo = $this->get_stub_legacy_consumer_repo(); } else { $legacyconsumerrepo = new legacy_consumer_repository(); } if (!empty($expected['exception'])) { $this->expectException($expected['exception']); $this->expectExceptionMessage($expected['exceptionmessage']); new migration_claim( $lti1p1migrationclaim, $deploymentid, $platform, $clientid, $exp, $nonce, $legacyconsumerrepo, ); } else { $migrationclaim = new migration_claim( $lti1p1migrationclaim, $deploymentid, $platform, $clientid, $exp, $nonce, $legacyconsumerrepo, ); $this->assertInstanceOf(migration_claim::class, $migrationclaim); $this->assertEquals($expected['user_id'], $migrationclaim->get_user_id()); $this->assertEquals($expected['context_id'], $migrationclaim->get_context_id()); $this->assertEquals( $expected['tool_consumer_instance_guid'], $migrationclaim->get_tool_consumer_instance_guid(), ); $this->assertEquals($expected['resource_link_id'], $migrationclaim->get_resource_link_id()); } } /** * Data provider testing migration_claim instantiation. * * @return array[] the test case data. */ public static function migration_claim_provider(): array { // Note: See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim for details regarding the // correct generation of oauth_consumer_key_sign signature. return [ 'Invalid - missing oauth_consumer_key' => [ 'lti1p1migrationclaim' => [ 'oauth_consumer_key' => '', 'oauth_consumer_key_sign' => 'abcd', ], 'deploymentid' => 'D12345', 'platform' => 'https://lms.example.org/', 'clientid' => 'a1b2c3d4', 'exp' => '1622612930', 'nonce' => 'j45j2j5nnjn24544', 'stublegacyconsumerrepo' => false, 'expected' => [ 'exception' => \coding_exception::class, 'exceptionmessage' => "Missing 'oauth_consumer_key' property in lti1p1 migration claim." ] ], 'Invalid - missing oauth_consumer_key_sign' => [ 'lti1p1migrationclaim' => [ 'oauth_consumer_key' => 'CONSUMER_1', 'oauth_consumer_key_sign' => '', ], 'deploymentid' => 'D12345', 'platform' => 'https://lms.example.org/', 'clientid' => 'a1b2c3d4', 'exp' => '1622612930', 'nonce' => 'j45j2j5nnjn24544', 'stublegacyconsumerrepo' => false, 'expected' => [ 'exception' => \coding_exception::class, 'exceptionmessage' => "Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim." ] ], 'Invalid - incorrect oauth_consumer_key_sign' => [ 'lti1p1migrationclaim' => [ 'oauth_consumer_key' => 'CONSUMER_1', 'oauth_consumer_key_sign' => 'badsignature', ], 'deploymentid' => 'D12345', 'platform' => 'https://lms.example.org/', 'clientid' => 'a1b2c3d4', 'exp' => '1622612930', 'nonce' => 'j45j2j5nnjn24544', 'stublegacyconsumerrepo' => false, 'expected' => [ 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim." ] ], 'Valid - signature valid, map properties not provided' => [ 'lti1p1migrationclaim' => [ 'oauth_consumer_key' => 'CONSUMER_1', 'oauth_consumer_key_sign' => base64_encode( hash_hmac( 'sha256', 'CONSUMER_1&D12345&https://lms.example.org/&a1b2c3d4&1622612930&j45j2j5nnjn24544', 'consumer_secret' ) ), ], 'deploymentid' => 'D12345', 'platform' => 'https://lms.example.org/', 'clientid' => 'a1b2c3d4', 'exp' => '1622612930', 'nonce' => 'j45j2j5nnjn24544', 'stublegacyconsumerrepo' => true, 'expected' => [ 'user_id' => null, 'context_id' => null, 'tool_consumer_instance_guid' => null, 'resource_link_id' => null ] ], 'Valid - signature valid, map properties are provided' => [ 'lti1p1migrationclaim' => [ 'oauth_consumer_key' => 'CONSUMER_1', 'oauth_consumer_key_sign' => base64_encode( hash_hmac( 'sha256', 'CONSUMER_1&D12345&https://lms.example.org/&a1b2c3d4&1622612930&j45j2j5nnjn24544', 'consumer_secret' ) ), 'user_id' => '24', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], 'deploymentid' => 'D12345', 'platform' => 'https://lms.example.org/', 'clientid' => 'a1b2c3d4', 'exp' => '1622612930', 'nonce' => 'j45j2j5nnjn24544', 'stublegacyconsumerrepo' => true, 'expected' => [ 'user_id' => '24', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ] ]; } } tests/local/ltiadvantage/entity/context_test.php000064400000016553152266447110016235 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for context. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\context */ final class context_test extends \advanced_testcase { /** * Test creation of the object instances. * * @dataProvider instantiation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_creation(array $args, array $expectations): void { if (!$expectations['valid']) { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); context::create(...array_values($args)); } else { $context = context::create(...array_values($args)); $this->assertEquals($args['deploymentid'], $context->get_deploymentid()); $this->assertEquals($args['contextid'], $context->get_contextid()); $this->assertEquals($args['types'], $context->get_types()); $this->assertEquals($args['id'], $context->get_id()); } } /** * Data provider for testing object instantiation. * @return array[] the data for testing. */ public static function instantiation_data_provider(): array { return [ 'Creation of a course section context' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a course offering context' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a course template context' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a course group context' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'http://purl.imsglobal.org/vocab/lis/v2/course#Group' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of an invalid context' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'http://example.com/invalid/context' ], 'id' => null ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Cannot set invalid context type 'http://example.com/invalid/context'." ] ], 'Creation of a simple name context with type CourseTemplate' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'CourseTemplate' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a simple name context with type CourseOffering' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'CourseOffering' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a simple name context with type CourseSection' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'CourseSection' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a simple name context with type Group' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'Group' ], 'id' => null ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a context with id' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'Group' ], 'id' => 24 ], 'expectations' => [ 'valid' => true, ] ], 'Creation of a context with invalid id' => [ 'args' => [ 'deploymentid' => 24, 'contextid' => 'context-123', 'types' => [ 'Group' ], 'id' => 0 ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "id must be a positive int" ] ], ]; } } tests/local/ltiadvantage/entity/resource_link_test.php000064400000026006152266447110017407 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for resource_link. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\resource_link */ final class resource_link_test extends \advanced_testcase { /** * Test creation of the object instances. * * @dataProvider instantiation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_create(array $args, array $expectations): void { if (!$expectations['valid']) { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); resource_link::create(...array_values($args)); } else { $reslink = resource_link::create(...array_values($args)); $this->assertEquals($args['resourcelinkid'], $reslink->get_resourcelinkid()); $this->assertEquals($args['resourceid'], $reslink->get_resourceid()); $this->assertEquals($args['deploymentid'], $reslink->get_deploymentid()); $this->assertEquals($args['contextid'], $reslink->get_contextid()); $this->assertEquals($args['id'], $reslink->get_id()); $this->assertEquals(null, $reslink->get_grade_service()); $this->assertEquals(null, $reslink->get_names_and_roles_service()); } } /** * Data provider for testing object instantiation. * @return array the data for testing. */ public static function instantiation_data_provider(): array { return [ 'Valid creation, no context or id provided' => [ 'args' => [ 'resourcelinkid' => 'res-link-id-123', 'deploymentid' => 45, 'resourceid' => 24, 'contextid' => null, 'id' => null ], 'expectations' => [ 'valid' => true ] ], 'Valid creation, context id provided, no id provided' => [ 'args' => [ 'resourcelinkid' => 'res-link-id-123', 'deploymentid' => 45, 'resourceid' => 24, 'contextid' => 777, 'id' => null ], 'expectations' => [ 'valid' => true ] ], 'Valid creation, context id and id provided' => [ 'args' => [ 'resourcelinkid' => 'res-link-id-123', 'deploymentid' => 45, 'resourceid' => 24, 'contextid' => 777, 'id' => 33 ], 'expectations' => [ 'valid' => true ] ], 'Invlid creation, empty resource link id string' => [ 'args' => [ 'resourcelinkid' => '', 'deploymentid' => 45, 'resourceid' => 24, 'contextid' => null, 'id' => null ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Error: resourcelinkid cannot be an empty string' ] ] ]; } /** * Test confirming that a grade service instance can be added to the object instance. * * @param array $args the array of method arguments * @param array $expected the array of expectations * @dataProvider add_grade_service_provider * @covers ::add_grade_service */ public function test_add_grade_service(array $args, array $expected): void { $reslink = resource_link::create('res-link-id-123', 24, 44); $this->assertNull($reslink->get_grade_service()); if (!$expected['valid']) { $this->expectException($expected['exception']); } $reslink->add_grade_service(...array_values($args)); $gradeservice = $reslink->get_grade_service(); $this->assertInstanceOf(ags_info::class, $gradeservice); $this->assertEquals($args['lineitemsurl'], $gradeservice->get_lineitemsurl()); $this->assertEquals($args['lineitemurl'], $gradeservice->get_lineitemurl()); $this->assertEquals($args['scope'], $gradeservice->get_scopes()); } /** * Data provider for testing the add_grade_service method. * * @return array the array of test case data. */ public static function add_grade_service_provider(): array { return [ 'Valid, both URLs, some scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expected' => [ 'valid' => true, ] ], 'Valid, only coupled line item URL, some scopes' => [ 'args' => [ 'lineitemsurl' => null, 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expected' => [ 'valid' => true, ] ], 'Valid, only decoupled line items URL, some scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score', ] ], 'expected' => [ 'valid' => true, ] ], 'Valid, URLs without any scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scope' => [] ], 'expected' => [ 'valid' => true, ] ], 'Invalid, missing both URLs' => [ 'args' => [ 'lineitemsurl' => null, 'lineitemurl' => null, 'scope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem' ] ], 'expected' => [ 'valid' => false, 'exception' => \coding_exception::class ] ], ]; } /** * Test confirming that a names and roles service instance can be added to the object instance. * * @covers ::add_names_and_roles_service */ public function test_add_names_and_roles_service(): void { $reslink = resource_link::create('res-link-id-123', 24, 44); $this->assertNull($reslink->get_names_and_roles_service()); $reslink->add_names_and_roles_service(new \moodle_url('https://lms.example.com/10/memberships'), ['2.0']); $nrps = $reslink->get_names_and_roles_service(); $this->assertInstanceOf(nrps_info::class, $nrps); $this->assertEquals(new \moodle_url('https://lms.example.com/10/memberships'), $nrps->get_context_memberships_url()); $this->assertEquals(['2.0'], $nrps->get_service_versions()); } /** * Verify that a user can be created from a resource link that has an id. * * @covers ::add_user */ public function test_add_user(): void { global $CFG; $reslinkwithid = resource_link::create('res-link-id-123', 24, 44, 66, 33); $user = $reslinkwithid->add_user(2, 'platform-user-id-123', $CFG->lang, 'Sydney', 'AU', 'Test university', '99'); $this->assertInstanceOf(user::class, $user); $this->assertEquals(33, $user->get_resourcelinkid()); $reslinkwithoutid = resource_link::create('res-link-id-123', 24, 44); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Can't add user to a resource_link that hasn't first been saved"); $reslinkwithoutid->add_user(2, 'platform-user-id-123', $CFG->lang, 'Sydney', 'Australia', 'Test university', '99'); } /** * Test confirming that the resourceid can be changed on the object. * * @covers ::set_resourceid */ public function test_set_resource_id(): void { $reslink = resource_link::create('res-link-id-123', 24, 44); $this->assertEquals(44, $reslink->get_resourceid()); $reslink->set_resourceid(333); $this->assertEquals(333, $reslink->get_resourceid()); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Resource id must be a positive int'); $reslink->set_resourceid(0); } /** * Test confirming that the contextid can be changed on the object. * * @covers ::set_contextid */ public function test_set_context_id(): void { $reslink = resource_link::create('res-link-id-123', 24, 44); $this->assertEquals(null, $reslink->get_contextid()); $reslink->set_contextid(333); $this->assertEquals(333, $reslink->get_contextid()); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Context id must be a positive int'); $reslink->set_contextid(0); } } tests/local/ltiadvantage/entity/application_registration_test.php000064400000040766152266447110021651 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for application_registration. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\application_registration */ final class application_registration_test extends \advanced_testcase { /** * Test the creation of an application_registration instance. * * @dataProvider creation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_creation(array $args, array $expectations): void { if ($expectations['valid']) { $reg = application_registration::create(...array_values($args)); $this->assertEquals($args['name'], $reg->get_name()); $this->assertEquals($args['uniqueid'], $reg->get_uniqueid()); $this->assertEquals($args['platformid'], $reg->get_platformid()); $this->assertEquals($args['clientid'], $reg->get_clientid()); $this->assertEquals($args['authrequesturl'], $reg->get_authenticationrequesturl()); $this->assertEquals($args['jwksurl'], $reg->get_jwksurl()); $this->assertEquals($args['accesstokenurl'], $reg->get_accesstokenurl()); $expectedid = $args['id'] ?? null; $this->assertEquals($expectedid, $reg->get_id()); $this->assertTrue($reg->is_complete()); } else { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); application_registration::create(...array_values($args)); } } /** * Data provider for testing the creation of application_registration instances. * * @return array the data for testing. */ public static function creation_data_provider(): array { return [ 'Valid, only required args provided' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ], 'expectations' => [ 'valid' => true ] ], 'Valid, all args provided' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), 'id' => 24 ], 'expectations' => [ 'valid' => true ] ], 'Invalid, empty name provided' => [ 'args' => [ 'name' => '', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'name' arg. Cannot be an empty string." ] ], 'Invalid, empty uniqueid provided' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => '', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'uniqueid' arg. Cannot be an empty string." ] ], 'Invalid, empty clientid provided' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => '', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'clientid' arg. Cannot be an empty string." ] ] ]; } /** * Test the creation of a draft application_registration instances. * * @dataProvider create_draft_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create_draft */ public function test_create_draft(array $args, array $expectations): void { if ($expectations['valid']) { $reg = application_registration::create_draft(...array_values($args)); $this->assertEquals($args['name'], $reg->get_name()); $this->assertEquals($args['uniqueid'], $reg->get_uniqueid()); $this->assertNull($reg->get_platformid()); $this->assertNull($reg->get_clientid()); $this->assertNull($reg->get_authenticationrequesturl()); $this->assertNull($reg->get_jwksurl()); $this->assertNull($reg->get_accesstokenurl()); $expectedid = $args['id'] ?? null; $this->assertEquals($expectedid, $reg->get_id()); $this->assertFalse($reg->is_complete()); } else { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); application_registration::create_draft(...array_values($args)); } } /** * Test that complete registration can transition a pending registration in the correct state to a complete registration. * * @covers ::complete_registration */ public function test_complete_registration(): void { // Create a draft registration which should initially be incomplete. $draft = application_registration::create_draft('Test platform name', '1234bfda'); $this->assertFalse($draft->is_complete()); // Add information to the draft, but don't complete it. It should still be incomplete. $draft->set_name('Adjusted platform name'); $draft->set_platformid(new \moodle_url('https://lms.example.com')); $draft->set_clientid('bcgd23'); $draft->set_authenticationrequesturl(new \moodle_url('https://lms.example.com/auth')); $draft->set_jwksurl(new \moodle_url('https://lms.example.com/jwks')); $draft->set_accesstokenurl(new \moodle_url('https://lms.example.com/token')); $this->assertFalse($draft->is_complete()); // Complete the registration. It will now be indicated as complete. $draft->complete_registration(); $this->assertTrue($draft->is_complete()); // Now, create another draft registration and try to mark it complete. It should throw. $draft2 = application_registration::create_draft('Another platform', '3434bfafas'); $this->expectException(\coding_exception::class); $draft2->complete_registration(); } /** * Data provider for testing draft creation. * * @return array the test case data. */ public static function create_draft_data_provider(): array { return [ 'Valid, new draft' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', ], 'expectations' => [ 'valid' => true ] ], 'Valid, existing draft' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'id' => 24 ], 'expectations' => [ 'valid' => true ] ], 'Invalid, empty identifier' => [ 'args' => [ 'name' => 'Platform X', 'uniqueid' => '', ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'uniqueid' arg. Cannot be an empty string." ] ] ]; } /** * Test the factory method for creating a tool deployment associated with the registration instance. * * @dataProvider add_tool_deployment_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::add_tool_deployment */ public function test_add_tool_deployment(array $args, array $expectations): void { if ($expectations['valid']) { $reg = application_registration::create(...array_values($args['registration'])); $deployment = $reg->add_tool_deployment(...array_values($args['deployment'])); $this->assertInstanceOf(deployment::class, $deployment); $this->assertEquals($args['deployment']['name'], $deployment->get_deploymentname()); $this->assertEquals($args['deployment']['deploymentid'], $deployment->get_deploymentid()); $this->assertEquals($reg->get_id(), $deployment->get_registrationid()); } else { $reg = application_registration::create(...array_values($args['registration'])); $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); $reg->add_tool_deployment(...array_values($args['deployment'])); } } /** * Data provider for testing the add_tool_deployment method. * * @return array the array of test data. */ public static function add_tool_deployment_data_provider(): array { return [ 'Valid, contains id on registration and valid deployment data provided' => [ 'args' => [ 'registration' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), 'id' => 24 ], 'deployment' => [ 'name' => 'Deployment at site level', 'deploymentid' => 'id-123abc' ] ], 'expectations' => [ 'valid' => true, ] ], 'Invalid, missing id on registration' => [ 'args' => [ 'registration' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ], 'deployment' => [ 'name' => 'Deployment at site level', 'deploymentid' => 'id-123abc' ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Can't add deployment to a resource_link that hasn't first been saved." ] ], 'Invalid, id present on registration but empty deploymentname' => [ 'args' => [ 'registration' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), 'id' => 24 ], 'deployment' => [ 'name' => '', 'deploymentid' => 'id-123abc' ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'deploymentname' arg. Cannot be an empty string." ] ], 'Invalid, id present on registration but empty deploymentid' => [ 'args' => [ 'registration' => [ 'name' => 'Platform X', 'uniqueid' => 'a2c94a2c94', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'client-id-12345', 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), 'id' => 24 ], 'deployment' => [ 'name' => 'Site deployment', 'deploymentid' => '' ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Invalid 'deploymentid' arg. Cannot be an empty string." ] ] ]; } } tests/local/ltiadvantage/entity/ags_info_test.php000064400000036675152266447110016345 0ustar00. namespace enrol_lti\local\ltiadvantage\entity; /** * Tests for ags_info. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\ags_info */ final class ags_info_test extends \advanced_testcase { /** * Test creation of the object instances. * @dataProvider instantiation_data_provider * @param array $args the arguments to the creation method. * @param array $expectations various expectations for the test cases. * @covers ::create */ public function test_creation(array $args, array $expectations): void { if (!$expectations['valid']) { $this->expectException($expectations['exception']); $this->expectExceptionMessage($expectations['exceptionmessage']); ags_info::create(...array_values($args)); } else { $agsinfo = ags_info::create(...array_values($args)); $this->assertEquals($args['lineitemsurl'], $agsinfo->get_lineitemsurl()); $this->assertEquals($args['lineitemurl'], $agsinfo->get_lineitemurl()); if (isset($expectations['scopes'])) { $this->assertEquals($expectations['scopes'], $agsinfo->get_scopes()); } else { $this->assertEquals($args['scopes'], $agsinfo->get_scopes()); } $this->assertEquals($expectations['lineitemscope'], $agsinfo->get_lineitemscope()); $this->assertEquals($expectations['scorescope'], $agsinfo->get_scorescope()); $this->assertEquals($expectations['resultscope'], $agsinfo->get_resultscope()); } } /** * Data provider for testing object instantiation. * @return array the data for testing. */ public static function instantiation_data_provider(): array { return [ 'Both lineitems and lineitem URL provided with full list of valid scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' ], 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'Both lineitems and lineitem URL provided with lineitem scopes only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', ], 'scorescope' => null, 'resultscope' => null ] ], 'Both lineitems and lineitem URL provided with score scope only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score', 'resultscope' => null ] ], 'Both lineitems and lineitem URL provided with result scope only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => null, 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' ] ], 'Both lineitems and lineitem URL provided with no scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => null, 'resultscope' => null ] ], 'Just lineitems URL, no lineitem URL, with full list of valid scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' ], 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'Just lineitems URL, no lineitem URL, with lineitems scopes only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', ], 'scorescope' => null, 'resultscope' => null ] ], 'Just lineitems URL, no lineitem URL, with score scope only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score', 'resultscope' => null ] ], 'Just lineitems URL, no lineitem URL, with result scope only' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => null, 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' ] ], 'Just lineitems URL, no lineitem URL, with no scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => null, 'scopes' => [] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => null, 'resultscope' => null ] ], 'Both lineitems and lineitem URL provided with non-string scope' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score', 12345 ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => 'Scope must be a string value' ] ], 'Both lineitems and lineitem URL provided with unsupported scopes' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score', 'https://example.com/unsupported/scope' ] ], 'expectations' => [ 'valid' => true, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score', ], 'lineitemscope' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' ], 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'Both lineitems and lineitem URL provided with invalid scope types' => [ 'args' => [ 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 12 ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Scope must be a string value" ] ], 'Claim contains a single lineitem URL only with valid scopes' => [ 'args' => [ 'lineitemsurl' => null, 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => true, 'lineitemscope' => null, 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score', 'resultscope' => null ] ], 'Claim contains no lineitems URL or lineitem URL' => [ 'args' => [ 'lineitemsurl' => null, 'lineitemurl' => null, 'scopes' => [ 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ], 'expectations' => [ 'valid' => false, 'exception' => \coding_exception::class, 'exceptionmessage' => "Missing lineitem or lineitems URL" ] ], ]; } } tests/local/ltiadvantage/service/tool_launch_service_test.php000064400000115007152266447110020716 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use core_availability\info_module; use enrol_lti\local\ltiadvantage\entity\resource_link; use enrol_lti\local\ltiadvantage\entity\user; use enrol_lti\local\ltiadvantage\entity\context; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the tool_launch_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\service\tool_launch_service */ final class tool_launch_service_test extends \lti_advantage_testcase { /** * Test the use case "A user launches a tool so they can view an external resource/activity". * * @dataProvider user_launch_provider * @param array|null $legacydata array detailing what legacy information to create, or null if not required. * @param array|null $launchdata array containing details of the launch, including user and migration claim. * @param array $expected the array detailing expectations. * @covers ::user_launches_tool */ public function test_user_launches_tool(?array $legacydata, ?array $launchdata, array $expected): void { $this->resetAfterTest(); // Setup. $contextrepo = new context_repository(); $resourcelinkrepo = new resource_link_repository(); $deploymentrepo = new deployment_repository(); $userrepo = new user_repository(); [ $course, $modresource, $modresource2, $courseresource, $registration, $deployment ] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); // Generate the legacy data, on which the user migration is based. if ($legacydata) { [$legacytools, $legacyconsumer, $legacyusers] = $this->setup_legacy_data($course, $legacydata); } // Get a mock 1.3 launch, optionally including the lti1p1 migration claim based on a legacy tool secret. $mocklaunch = $this->get_mock_launch($modresource, $launchdata['user'], null, [], true, $launchdata['launch_migration_claim']); // Call the service. $launchservice = $this->get_tool_launch_service(); if (isset($expected['exception'])) { $this->expectException($expected['exception']); $this->expectExceptionMessage($expected['exception_message']); } [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch); // As part of the launch, we expect to now have an lti-enrolled user who is recorded against the deployment. $users = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $users); $user = array_pop($users); $this->assertInstanceOf(user::class, $user); $this->assertEquals($deployment->get_id(), $user->get_deploymentid()); // Deployment should be mapped to the legacy consumer key even if the user wasn't matched and migrated. $updateddeployment = $deploymentrepo->find($deployment->get_id()); $this->assertEquals($expected['deployment_consumer_key'], $updateddeployment->get_legacy_consumer_key()); // The user comes from a resource_link, details of which should also be saved and linked to the deployment. $resourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id()); $this->assertCount(1, $resourcelinks); $resourcelink = array_pop($resourcelinks); $this->assertInstanceOf(resource_link::class, $resourcelink); $this->assertEquals($deployment->get_id(), $resourcelink->get_deploymentid()); // The resourcelink should have a context, which should also be saved and linked to the deployment. $context = $contextrepo->find($resourcelink->get_contextid()); $this->assertInstanceOf(context::class, $context); $this->assertEquals($deployment->get_id(), $context->get_deploymentid()); $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(1, $enrolledusers); // Verify the module is visible to the user. $cmcontext = \context::instance_by_id($modresource->contextid); $this->assertTrue(info_module::is_user_visible($cmcontext->instanceid, $userid)); // And that other published modules are not yet visible to the user. $cmcontext = \context::instance_by_id($modresource2->contextid); $this->assertFalse(info_module::is_user_visible($cmcontext->instanceid, $userid)); } /** * Provider for user launch testing. * * @return array the test case data. */ public static function user_launch_provider(): array { return [ 'New tool: no legacy data, no migration claim sent' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'expected' => [ 'deployment_consumer_key' => null, ] ], 'Migrated tool: Legacy data exists, no change in user_id so omitted from claim' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'expected' => [ 'deployment_consumer_key' => 'CONSUMER_1', ] ], 'Migrated tool: Legacy data exists, platform signs with different valid secret' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'expected' => [ 'deployment_consumer_key' => 'CONSUMER_1', ] ], 'Migrated tool: Legacy data exists, no migration claim sent' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'expected' => [ 'deployment_consumer_key' => null, ] ], 'Migrated tool: Legacy data exists, migration claim signature generated using invalid secret' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'secret-not-mapped-to-consumer', 'user_id' => 'user-id-123', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'expected' => [ 'exception' => \coding_exception::class, 'exception_message' => "Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim" ] ], 'Migrated tool: Legacy data exists, migration claim signature omitted' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'user_id' => 'user-id-123', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'expected' => [ 'exception' => \coding_exception::class, 'exception_message' => "Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim." ] ], 'Migrated tool: Legacy data exists, migration claim missing oauth_consumer_key' => [ 'legacydata' => [ 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'user_id' => 'user-id-123', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'expected' => [ 'deployment_consumer_key' => null ] ] ]; } /** * Test confirming that an exception is thrown if trying to launch a published resource without a custom id. * * @covers ::user_launches_tool */ public function test_user_launches_tool_missing_custom_id(): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice = $this->get_tool_launch_service(); $mockuser = self::get_mock_launch_users_with_ids(['1p3_1'])[0]; $mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, null, false, null, []); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage(get_string('ltiadvlauncherror:missingid', 'enrol_lti')); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch); } /** * Test confirming that an exception is thrown if trying to launch a published resource that doesn't exist. * * @covers ::user_launches_tool */ public function test_user_launches_tool_invalid_custom_id(): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice = $this->get_tool_launch_service(); $mockuser = self::get_mock_launch_users_with_ids(['1p3_1'])[0]; $mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, null, false, null, ['id' => 999999]); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidid', 'enrol_lti', 999999)); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch); } /** * Test confirming that an exception is thrown if trying to launch the tool where no application can be found. * * @covers ::user_launches_tool */ public function test_user_launches_tool_missing_registration(): void { $this->resetAfterTest(); // Setup. [ $course, $modresource, $modresource2, $courseresource, $registration, $deployment ] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); // Delete the registration before trying to launch. $appregrepo = new application_registration_repository(); $appregrepo->delete($registration->get_id()); // Call the service. $launchservice = $this->get_tool_launch_service(); $mockuser = self::get_mock_launch_users_with_ids(['1p3_1'])[0]; $mocklaunch = $this->get_mock_launch($modresource, $mockuser); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti', [$registration->get_platformid(), $registration->get_clientid()])); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch); } /** * Test confirming that an exception is thrown if trying to launch the tool where no deployment can be found. * * @covers ::user_launches_tool */ public function test_user_launches_tool_missing_deployment(): void { $this->resetAfterTest(); // Setup. [ $course, $modresource, $modresource2, $courseresource, $registration, $deployment ] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); // Delete the deployment before trying to launch. $deploymentrepo = new deployment_repository(); $deploymentrepo->delete($deployment->get_id()); // Call the service. $launchservice = $this->get_tool_launch_service(); $mockuser = self::get_mock_launch_users_with_ids(['1p3_1'])[0]; $mocklaunch = $this->get_mock_launch($modresource, $mockuser); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage(get_string('ltiadvlauncherror:invaliddeployment', 'enrol_lti', [$deployment->get_deploymentid()])); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch); } /** * Test the mapping from IMS roles to Moodle roles during a launch. * * @covers ::user_launches_tool */ public function test_user_launches_tool_role_mapping(): void { $this->resetAfterTest(); // Create mock launches for 3 different user types: instructor, admin, learner. [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $instructor2user = $this->getDataGenerator()->create_user(); $adminuser = $this->getDataGenerator()->create_user(); $learneruser = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $mockadminuser = self::get_mock_launch_users_with_ids( ['2'], false, 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator' )[0]; $mocklearneruser = self::get_mock_launch_users_with_ids( ['3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0]; $mockinstructor2user = self::get_mock_launch_users_with_ids( ['3'], false, 'Instructor' // Using the legacy (deprecated in 1.3) simple name. )[0]; $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser); $mockadminlaunch = $this->get_mock_launch($modresource, $mockadminuser); $mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser); $mockinstructor2launch = $this->get_mock_launch($modresource, $mockinstructor2user); // Launch and confirm the role assignment. $launchservice = $this->get_tool_launch_service(); $modulecontext = \context::instance_by_id($modresource->contextid); [$instructorid] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); [$instructorrole] = array_slice(get_user_roles($modulecontext, $instructorid), 0, 1); $this->assertEquals('teacher', $instructorrole->shortname); [$adminid] = $launchservice->user_launches_tool($adminuser, $mockadminlaunch); [$adminrole] = array_slice(get_user_roles($modulecontext, $adminid), 0, 1); $this->assertEquals('teacher', $adminrole->shortname); [$learnerid] = $launchservice->user_launches_tool($learneruser, $mocklearnerlaunch); [$learnerrole] = array_slice(get_user_roles($modulecontext, $learnerid), 0, 1); $this->assertEquals('student', $learnerrole->shortname); [$instructor2id] = $launchservice->user_launches_tool($instructor2user, $mockinstructor2launch); [$instructor2role] = array_slice(get_user_roles($modulecontext, $instructor2id), 0, 1); $this->assertEquals('teacher', $instructor2role->shortname); } /** * Test verifying that a user launch can result in updates to some user fields. * * @covers ::user_launches_tool */ public function test_user_launches_tool_user_fields_updated(): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $user = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $launchservice = $this->get_tool_launch_service(); $userrepo = new user_repository(); // Launch once, verifying the user details. $mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser); $launchservice->user_launches_tool($user, $mocklaunch); $createduser = $userrepo->find_single_user_by_resource( $user->id, $modresource->id ); $this->assertEquals($modresource->lang, $createduser->get_lang()); $this->assertEquals($modresource->city, $createduser->get_city()); $this->assertEquals($modresource->country, $createduser->get_country()); $this->assertEquals($modresource->institution, $createduser->get_institution()); $this->assertEquals($modresource->timezone, $createduser->get_timezone()); $this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay()); // Change the resource's defaults and relaunch, verifying the relevant fields are updated for the launch user. // Note: lang change can't be tested without installation of another language pack. $modresource->city = 'Paris'; $modresource->country = 'FR'; $modresource->institution = 'Updated institution name'; $modresource->timezone = 'UTC'; $modresource->maildisplay = '1'; global $DB; $DB->update_record('enrol_lti_tools', $modresource); $mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser); $launchservice->user_launches_tool($user, $mocklaunch); $createduser = $userrepo->find($createduser->get_id()); $this->assertEquals($modresource->city, $createduser->get_city()); $this->assertEquals($modresource->country, $createduser->get_country()); $this->assertEquals($modresource->institution, $createduser->get_institution()); $this->assertEquals($modresource->timezone, $createduser->get_timezone()); $this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay()); } /** * Test the launch when a module has an enrolment start date. * * @covers ::user_launches_tool */ public function test_user_launches_tool_max_enrolment_start_restriction(): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(true, true, false, \enrol_lti\helper::MEMBER_SYNC_ENROL_NEW, false, false, time() + DAYSECS); $instructoruser = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser); $launchservice = $this->get_tool_launch_service(); $this->expectException(\moodle_exception::class); $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); } /** * Test the Moodle-specific custom param 'forceembed' during user launches. * * @covers ::user_launches_tool */ public function test_user_launches_tool_force_embedding_custom_param(): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $learneruser = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $mocklearneruser = self::get_mock_launch_users_with_ids(['1'], false, '')[0]; $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, null, false, null, [ 'id' => $modresource->uuid, 'forcedembed' => true ]); $mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser, null, null, false, null, [ 'id' => $modresource->uuid, 'forcedembed' => true ]); $launchservice = $this->get_tool_launch_service(); global $SESSION; // Instructors aren't subject to forceembed. $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); $this->assertObjectNotHasProperty('forcepagelayout', $SESSION); // Learners are. $launchservice->user_launches_tool($learneruser, $mocklearnerlaunch); $this->assertEquals('embedded', $SESSION->forcepagelayout); } /** * Test launching the tool with different 'aud' values, confirming the service handles all variations appropriately. * * @param mixed $aud the aud value to test * @param array $expected the array of expectations to check * @dataProvider aud_data_provider * @covers ::user_launches_tool */ public function test_user_launches_tool_aud_variations($aud, array $expected): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, null, false, null, [ 'id' => $modresource->uuid, ], $aud); $launchservice = $this->get_tool_launch_service(); if (isset($expected['exception'])) { $this->expectException($expected['exception']); $this->expectExceptionMessage($expected['exceptionmessage']); } [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); $this->assertNotEmpty($userid); $this->assertNotEmpty($resource); } /** * Data provider for testing variations of the 'aud' JWT property. * * @return array the test case data */ public static function aud_data_provider(): array { return [ 'valid, array having multiple entries with the first one being clientid' => [ 'aud' => ['123', 'something else', 'blah'], 'expected' => [ 'valid' => true ] ], 'valid, array having a single entry being clientid' => [ 'aud' => ['123'], 'expected' => [ 'valid' => true ] ], 'valid, string containing the single clientid' => [ 'aud' => '123', 'expected' => [ 'valid' => true ] ], 'invalid, array having multiple values where the first item is not clientid' => [ 'aud' => ['cat', 'dog', '123'], 'expected' => [ 'valid' => false, 'exception' => \moodle_exception::class, 'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti') ] ], 'invalid, array containing a single item which is not clientid' => [ 'aud' => ['cat'], 'expected' => [ 'valid' => false, 'exception' => \moodle_exception::class, 'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti') ] ], 'invalid, string contains the item and it is not the clientid' => [ 'aud' => 'cat', 'expected' => [ 'valid' => false, 'exception' => \moodle_exception::class, 'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti') ] ], 'invalid, empty string' => [ 'aud' => '', 'expected' => [ 'valid' => false, 'exception' => \moodle_exception::class, 'exceptionmessage' => get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti') ] ], ]; } /** * Test verifying how changes to lti-ags claim information is handled across different launches. * * @param array $agsclaim1 the lti-ags claim data to use in the first launch. * @param array $agsclaim2 the lti-ags claim data to use in the second launch. * @param array $expected the array of test case expectations. * @dataProvider ags_claim_provider * @covers ::user_launches_tool */ public function test_user_launches_tool_ags_claim_handling(array $agsclaim1, array $agsclaim2, array $expected): void { $this->resetAfterTest(); [$course, $modresource] = $this->create_test_environment(); $instructoruser = $this->getDataGenerator()->create_user(); $mockinstructoruser = self::get_mock_launch_users_with_ids(['1'])[0]; $userrepo = new user_repository(); $resourcelinkrepo = new resource_link_repository(); $launchservice = $this->get_tool_launch_service(); // Launch the first time. $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, $agsclaim1, false, null, [ 'id' => $modresource->uuid, ]); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); $ltiuser = $userrepo->find_single_user_by_resource($userid, $resource->id); $resourcelink = $resourcelinkrepo->find_by_resource_and_user($resource->id, $ltiuser->get_id())[0]; $gradeservice = $resourcelink->get_grade_service(); $lineitemurl = $agsclaim1['lineitem'] ?? null; $lineitemsurl = $agsclaim1['lineitems'] ?? null; $this->assertEquals($agsclaim1['scope'], $gradeservice->get_scopes()); $this->assertEquals($lineitemurl, $gradeservice->get_lineitemurl()); $this->assertEquals($lineitemsurl, $gradeservice->get_lineitemsurl()); // Launch again, with a new lti-ags claim. $mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, $agsclaim2, false, null, [ 'id' => $modresource->uuid, ]); [$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch); $ltiuser = $userrepo->find_single_user_by_resource($userid, $resource->id); $resourcelink = $resourcelinkrepo->find_by_resource_and_user($resource->id, $ltiuser->get_id())[0]; $gradeservice = $resourcelink->get_grade_service(); $lineitemurl = $expected['lineitem'] ?? null; $lineitemsurl = $expected['lineitems'] ?? null; $this->assertEquals($expected['scope'], $gradeservice->get_scopes()); $this->assertEquals($lineitemurl, $gradeservice->get_lineitemurl()); $this->assertEquals($lineitemsurl, $gradeservice->get_lineitemsurl()); } /** * Data provider for testing user_launches tool with varying mocked lti-ags claim data over several launches. * * @return array the array of test case data. */ public static function ags_claim_provider(): array { return [ 'Coupled line item with score post only, no change to scopes on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ] ], 'Coupled line item with score post only, addition to scopes on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score", ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score", ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ] ], 'Coupled line item with score post + result read, removal of scopes on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score", ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score", ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ] ], 'Decoupled line items with all capabilities, change and removal of scopes on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ] ], 'Decoupled line items with all capabilities, removal of scopes on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ] ], 'Coupled line items with score post only, addition of lineitemsurl and all capabilities on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ] ], 'Decoupled line items with all capabilities, change to coupled line item with score post only on subsequent launch' => [ 'agsclaim1' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems" => "https://platform.example.com/10/lineitems/", ], 'agsclaim2' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ], 'expected' => [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ] ], ]; } } tests/local/ltiadvantage/service/tool_deployment_service_test.php000064400000016502152266447110021624 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use enrol_lti\helper; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\entity\deployment; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the tool_deployment_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\service\tool_deployment_service */ final class tool_deployment_service_test extends \lti_advantage_testcase { /** * Return a pre-existing application_registration object for testing. * * @return application_registration */ protected function generate_application_registration(): application_registration { $reg = application_registration::create( 'Example LMS application', 'a2c94a2c94', new \moodle_url('https://lms.example.org'), '123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $regrepo = new application_registration_repository(); return $regrepo->save($reg); } /** * Helper to get a tool_deployment_service instance. * * @return tool_deployment_service the instance. */ protected function get_tool_deployment_service(): tool_deployment_service { return new tool_deployment_service( new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository() ); } /** * Test the use case "As an admin, I can register an application as an LTI consumer (platform)". * * @covers ::add_tool_deployment */ public function test_add_tool_deployment(): void { $this->resetAfterTest(); $testreg = $this->generate_application_registration(); $deploymentrepo = new deployment_repository(); $service = $this->get_tool_deployment_service(); $createddeployment = $service->add_tool_deployment( (object) [ 'registration_id' => $testreg->get_id(), 'deployment_id' => 'Deploy_ID_123', 'deployment_name' => "Tool deployment in location x" ] ); $this->assertInstanceOf(deployment::class, $createddeployment); $this->assertTrue($deploymentrepo->exists($createddeployment->get_id())); } /** * Test trying to add a tool deployment when a registration identified by the specified id cannot be found. * * @covers ::add_tool_deployment */ public function test_add_tool_deployment_registration_missing(): void { $this->resetAfterTest(); $service = $this->get_tool_deployment_service(); $this->expectException(\coding_exception::class); $this->expectExceptionMessageMatches('/Cannot add deployment to non-existent application registration/'); $service->add_tool_deployment( (object) [ 'registration_id' => 1234, 'deployment_id' => 'Deploy_ID_123', 'deployment_name' => "Tool deployment in location x" ] ); } /** * Test that removal of a deployment removes all associated data. * * @covers ::delete_tool_deployment */ public function test_delete_deployment(): void { $this->resetAfterTest(); // Setup. $registrationrepo = new application_registration_repository(); $deploymentrepo = new deployment_repository(); $contextrepo = new context_repository(); $resourcelinkrepo = new resource_link_repository(); $userrepo = new user_repository(); [$course, $resource] = $this->create_test_environment(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice = $this->get_tool_launch_service(); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Check all the expected data exists for the deployment after setup. $registrations = $registrationrepo->find_all(); $this->assertCount(1, $registrations); $registration = array_pop($registrations); $deployments = $deploymentrepo->find_all_by_registration($registration->get_id()); $this->assertCount(1, $deployments); $deployment = array_pop($deployments); $resourcelinks = $resourcelinkrepo->find_by_resource($resource->id); $this->assertCount(1, $resourcelinks); $resourcelink = array_pop($resourcelinks); $context = $contextrepo->find($resourcelink->get_contextid()); $this->assertNotNull($context); $users = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $users); $user = array_pop($users); $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(1, $enrolledusers); // Now delete the deployment using the service. $service = $this->get_tool_deployment_service(); $service->delete_tool_deployment($deployment->get_id()); // Verify that the context, resourcelink, user and deployment instances are all deleted but the registration // instance remains. $this->assertTrue($registrationrepo->exists($registration->get_id())); $this->assertFalse($deploymentrepo->exists($deployment->get_id())); $this->assertFalse($contextrepo->exists($context->get_id())); $this->assertFalse($resourcelinkrepo->exists($resourcelink->get_id())); $this->assertFalse($userrepo->exists($user->get_id())); // Verify that all users are unenrolled. $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(0, $enrolledusers); // Verify the tool record stays in place (I.e. the published resource is still available). $this->assertNotEmpty(helper::get_lti_tool($resource->id)); } } tests/local/ltiadvantage/service/application_registration_service_test.php000064400000022167152266447110023510 0ustar00. namespace enrol_lti\local\ltiadvantage\service; use enrol_lti\helper; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\context_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the application_registration_service. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\service\application_registration_service */ final class application_registration_service_test extends \lti_advantage_testcase { /** * Helper to get an application_registration_service instance. * @return application_registration_service */ protected function get_application_registration_service(): application_registration_service { return new application_registration_service( new application_registration_repository(), new deployment_repository(), new resource_link_repository(), new context_repository(), new user_repository() ); } /** * Test the use case "As an admin, I can register an application as an LTI consumer (platform)". * * @covers ::create_draft_application_registration */ public function test_create_draft_application(): void { $this->resetAfterTest(); $service = $this->get_application_registration_service(); // Create a draft, passing the required name. $draftreg = $service->create_draft_application_registration((object) ['name' => 'My test platform']); $this->assertEquals('My test platform', $draftreg->get_name()); $this->assertNull($draftreg->get_authenticationrequesturl()); $this->assertNull($draftreg->get_jwksurl()); $this->assertNull($draftreg->get_accesstokenurl()); $this->assertNull($draftreg->get_platformid()); $this->assertNull($draftreg->get_clientid()); $this->assertFalse($draftreg->is_complete()); // Try to create a draft omitting name. $this->expectException(\coding_exception::class); $service->create_draft_application_registration((object) []); } /** * Test the update_application_registration method. * * @covers ::update_application_registration */ public function test_update_application_registration(): void { $this->resetAfterTest(); // Create a registration in the draft state. $service = $this->get_application_registration_service(); $draftreg = $service->create_draft_application_registration((object) ['name' => 'My test platform']); // Update the draft. $updatedto = (object) [ 'id' => $draftreg->get_id(), 'name' => 'My test platform name edit 1', 'platformid' => 'https://lms.example.org', 'clientid' => '123', 'authenticationrequesturl' => 'https://lms.example.org/authrequesturl', 'jwksurl' => 'https://lms.example.org/jwksurl', 'accesstokenurl' => 'https://lms.example.org/accesstokenurl' ]; $reg = $service->update_application_registration($updatedto); // Verify details saved and complete status. $this->assertEquals($updatedto->id, $reg->get_id()); $this->assertEquals($updatedto->name, $reg->get_name()); $this->assertEquals(new \moodle_url($updatedto->platformid), $reg->get_platformid()); $this->assertEquals($updatedto->clientid, $reg->get_clientid()); $this->assertEquals(new \moodle_url($updatedto->authenticationrequesturl), $reg->get_authenticationrequesturl()); $this->assertEquals(new \moodle_url($updatedto->jwksurl), $reg->get_jwksurl()); $this->assertEquals(new \moodle_url($updatedto->accesstokenurl), $reg->get_accesstokenurl()); $this->assertTrue($reg->is_complete()); // Update again. $updatedto = (object) [ 'id' => $draftreg->get_id(), 'name' => 'My test platform name edit 2', 'platformid' => 'https://different.example.org', 'clientid' => 'abcd1234', 'authenticationrequesturl' => 'https://lms.example.org/authrequesturl2', 'jwksurl' => 'https://lms.example.org/jwksurl2', 'accesstokenurl' => 'https://lms.example.org/accesstokenurl2' ]; $reg = $service->update_application_registration($updatedto); // Verify again. $this->assertEquals($updatedto->id, $reg->get_id()); $this->assertEquals($updatedto->name, $reg->get_name()); $this->assertEquals(new \moodle_url($updatedto->platformid), $reg->get_platformid()); $this->assertEquals($updatedto->clientid, $reg->get_clientid()); $this->assertEquals(new \moodle_url($updatedto->authenticationrequesturl), $reg->get_authenticationrequesturl()); $this->assertEquals(new \moodle_url($updatedto->jwksurl), $reg->get_jwksurl()); $this->assertEquals(new \moodle_url($updatedto->accesstokenurl), $reg->get_accesstokenurl()); $this->assertTrue($reg->is_complete()); // Update missing id. unset($updatedto->id); $this->expectException(\coding_exception::class); $service->update_application_registration($updatedto); } /** * Test that removing an application registration also removes all associated data. * * @covers ::delete_application_registration */ public function test_delete_application_registration(): void { $this->resetAfterTest(); // Setup. $registrationrepo = new application_registration_repository(); $deploymentrepo = new deployment_repository(); $contextrepo = new context_repository(); $resourcelinkrepo = new resource_link_repository(); $userrepo = new user_repository(); [$course, $resource] = $this->create_test_environment(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice = $this->get_tool_launch_service(); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Check all the expected data exists for the deployment after setup. $registrations = $registrationrepo->find_all(); $this->assertCount(1, $registrations); $registration = array_pop($registrations); $deployments = $deploymentrepo->find_all_by_registration($registration->get_id()); $this->assertCount(1, $deployments); $deployment = array_pop($deployments); $resourcelinks = $resourcelinkrepo->find_by_resource($resource->id); $this->assertCount(1, $resourcelinks); $resourcelink = array_pop($resourcelinks); $context = $contextrepo->find($resourcelink->get_contextid()); $this->assertNotNull($context); $users = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $users); $user = array_pop($users); $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(1, $enrolledusers); // Now delete the application_registration using the service. $service = $this->get_application_registration_service(); $service->delete_application_registration($registration->get_id()); // Verify that the context, resourcelink, user, deployment and registration instances are all deleted. $this->assertFalse($registrationrepo->exists($registration->get_id())); $this->assertFalse($deploymentrepo->exists($deployment->get_id())); $this->assertFalse($contextrepo->exists($context->get_id())); $this->assertFalse($resourcelinkrepo->exists($resourcelink->get_id())); $this->assertFalse($userrepo->exists($user->get_id())); // Verify that all users are unenrolled. $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(0, $enrolledusers); // Verify the tool record stays in place (I.e. the published resource is still available). $this->assertNotEmpty(helper::get_lti_tool($resource->id)); } } tests/local/ltiadvantage/repository/resource_link_repository_test.php000064400000042717152266447110022620 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\resource_link; use enrol_lti\local\ltiadvantage\entity\application_registration; /** * Tests for resource_link_repository objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\resource_link_repository */ final class resource_link_repository_test extends \advanced_testcase { /** * Helper to generate a new resource_link instance. * * @param string $id the id to use for this the resource link. * @return resource_link the resource_link instance. */ protected function generate_resource_link($id = 'res-link-1'): resource_link { $registration = application_registration::create( 'Test', 'a2c94a2c94', new \moodle_url('http://lms.example.org'), 'clientid_123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registrationrepo = new application_registration_repository(); $createdregistration = $registrationrepo->save($registration); $deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123'); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $contextrepo = new context_repository(); $context = $saveddeployment->add_context( 'CTX123', ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'] ); $savedcontext = $contextrepo->save($context); $resourcelink = $saveddeployment->add_resource_link($id, $savedcontext->get_id()); $resourcelink->add_grade_service( new \moodle_url('https://lms.example.com/context/24/lineitems'), new \moodle_url('https://lms.example.com/context/24/lineitem/3'), [ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', 'https://purl.imsglobal.org/spec/lti-ags/scope/score' ] ); $resourcelink->add_names_and_roles_service( new \moodle_url('https://lms.example.com/context/24/memberships'), [1.0, 2.0] ); return $resourcelink; } /** * Helper to assert that all the key elements of two resource_links (i.e. excluding id) are equal. * * @param resource_link $expected the resource_link whose values are deemed correct. * @param resource_link $check the resource_link to check. */ protected function assert_same_resourcelink_values(resource_link $expected, resource_link $check): void { $this->assertEquals($expected->get_resourcelinkid(), $check->get_resourcelinkid()); $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid()); $this->assertEquals($expected->get_contextid(), $check->get_contextid()); $this->assertEquals($expected->get_grade_service(), $check->get_grade_service()); $this->assertEquals($expected->get_names_and_roles_service(), $check->get_names_and_roles_service()); } /** * Helper to assert that all the key elements of a resource_link are present in the DB. * * @param resource_link $expected the resource_link whose values are deemed correct. */ protected function assert_resourcelink_db_values(resource_link $expected) { global $DB; $checkrecord = $DB->get_record('enrol_lti_resource_link', ['id' => $expected->get_id()]); $gradeservice = $expected->get_grade_service(); $this->assertEquals($expected->get_id(), $checkrecord->id); $this->assertEquals($expected->get_deploymentid(), $checkrecord->ltideploymentid); $this->assertEquals($expected->get_resourcelinkid(), $checkrecord->resourcelinkid); $this->assertEquals($expected->get_contextid(), $checkrecord->lticontextid); $this->assertEquals($gradeservice ? $gradeservice->get_lineitemsurl() : null, $checkrecord->lineitemsservice); $this->assertEquals($gradeservice ? $gradeservice->get_lineitemurl() : null, $checkrecord->lineitemservice); $this->assertEquals($gradeservice ? json_encode($gradeservice->get_lineitemscope()) : null, $checkrecord->lineitemscope); $this->assertEquals($gradeservice ? $gradeservice->get_resultscope() : null, $checkrecord->resultscope); $this->assertEquals($gradeservice ? $gradeservice->get_scorescope() : null, $checkrecord->scorescope); $this->assertNotEmpty($checkrecord->timecreated); $this->assertNotEmpty($checkrecord->timemodified); } /** * Tests adding a resource_link to the store. * * @covers ::save */ public function test_save_new(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $savedresourcelink = $repository->save($resourcelink); $this->assertIsInt($savedresourcelink->get_id()); $this->assert_same_resourcelink_values($resourcelink, $savedresourcelink); $this->assert_resourcelink_db_values($savedresourcelink); } /** * Test that we cannot add two resource_links with the same resourcelinkid for a given deploymentid. * * @covers ::save */ public function test_add_uniqueness_constraints(): void { $this->resetAfterTest(); $reslink1 = $this->generate_resource_link(); $reslink2 = clone $reslink1; $repository = new resource_link_repository(); $createdresource1 = $repository->save($reslink1); $this->assertIsInt($createdresource1->get_id()); $this->expectException(\dml_exception::class); $repository->save($reslink2); } /** * Test fetching an object from the store. * * @covers ::find */ public function test_find(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $locatedreslink = $repository->find($newreslink->get_id()); $this->assertEquals($newreslink, $locatedreslink); $repository->delete($locatedreslink->get_id()); $this->assertEmpty($repository->find($locatedreslink->get_id())); } /** * Test finding a collection of resource links by resource. * * @covers ::find_by_resource */ public function test_find_by_resource(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $resourcelink2 = resource_link::create('another-res-link-1', $newreslink->get_deploymentid(), $newreslink->get_resourceid()); $newreslink2 = $repository->save($resourcelink2); $resourcelink3 = resource_link::create('another-res-link-2', $newreslink->get_deploymentid(), $newreslink->get_resourceid() + 1); $newreslink3 = $repository->save($resourcelink3); $locatedreslinks = $repository->find_by_resource($newreslink->get_resourceid()); $this->assertCount(2, $locatedreslinks); usort($locatedreslinks, function($a, $b) { return strcmp($b->get_resourcelinkid(), $a->get_resourcelinkid()); }); $this->assertEquals([$newreslink, $newreslink2], $locatedreslinks); $locatedreslinks = $repository->find_by_resource($newreslink->get_resourceid() + 1); $this->assertCount(1, $locatedreslinks); $this->assertEquals([$newreslink3], $locatedreslinks); $this->assertEmpty($repository->find_by_resource(0)); } /** * Test finding a collection of resource links by resource and user. * * @covers ::find_by_resource_and_user */ public function test_find_by_resource_and_user(): void { global $CFG; $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $resourcelink2 = resource_link::create('another-res-link-1', $newreslink->get_deploymentid(), $newreslink->get_resourceid()); $newreslink2 = $repository->save($resourcelink2); $resourcelink3 = resource_link::create('another-res-link-2', $newreslink->get_deploymentid(), $newreslink->get_resourceid()); $newreslink3 = $repository->save($resourcelink3); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $userrepo = new user_repository(); $user = $newreslink->add_user( $user1->id, 'platform-user-id-123', $CFG->lang, 'Sydney', 'AU', 'Test university', '99' ); $createduser = $userrepo->save($user); $createduser->set_resourcelinkid($newreslink2->get_id()); $userrepo->save($createduser); $user2 = $newreslink3->add_user( $user2->id, 'platform-user-id-777', $CFG->lang, 'Melbourne', 'AU', 'Test university', '99' ); $createduser2 = $userrepo->save($user2); $locatedreslinks = $repository->find_by_resource_and_user($newreslink->get_resourceid(), $createduser->get_id()); $this->assertCount(2, $locatedreslinks); usort($locatedreslinks, function($a, $b) { return strcmp($b->get_resourcelinkid(), $a->get_resourcelinkid()); }); $this->assertEquals([$newreslink, $newreslink2], $locatedreslinks); $locatedreslinks = $repository->find_by_resource_and_user($newreslink->get_resourceid(), $createduser2->get_id()); $this->assertCount(1, $locatedreslinks); $this->assertEquals([$newreslink3], $locatedreslinks); $this->assertEmpty($repository->find_by_resource_and_user($newreslink->get_resourceid(), 0)); } /** * Test deletion from the store. * * @covers ::delete */ public function test_delete(): void { global $CFG; $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $this->assertTrue($repository->exists($newreslink->get_id())); // Also create a user from this resource link so we get some test user_resource_link mappings. $user = $newreslink->add_user( 2, 'source-id-123', $CFG->lang, 'Perth', 'AU', 'An Example Institution', '99', 2, ); $userrepo = new user_repository(); $userrepo->save($user); global $DB; $this->assertTrue($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); $repository->delete($newreslink->get_id()); $this->assertFalse($repository->exists($newreslink->get_id())); $this->assertEmpty($repository->find($newreslink->get_id())); $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); $this->assertNull($repository->delete($newreslink->get_id())); } /** * Test deleting a group of resource links by resource. * * @covers ::delete_by_resource */ public function test_delete_by_resource(): void { global $CFG; $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); // Also create a user from this resource link so we get some test user_resource_link mappings. $user = $newreslink->add_user( 2, 'source-id-123', $CFG->lang, 'Perth', 'AU', 'An Example Institution', '99', 2, ); $userrepo = new user_repository(); $userrepo->save($user); global $DB; $this->assertTrue($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); // Create a resource link under the same deployment for another resource. $resourcelink2 = resource_link::create('another-res-link-1', $newreslink->get_deploymentid(), $newreslink->get_resourceid() + 1); $newreslink2 = $repository->save($resourcelink2); $repository->delete_by_resource($newreslink->get_resourceid()); $this->assertFalse($repository->exists($newreslink->get_id())); $this->assertEmpty($repository->find($newreslink->get_id())); $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); $this->assertTrue($repository->exists($newreslink2->get_id())); $this->assertInstanceOf(resource_link::class, $repository->find($newreslink2->get_id())); $this->assertNull($repository->delete_by_deployment($newreslink->get_deploymentid())); } /** * Test deleting a resource links by their deployment container. * * @covers ::delete_by_deployment */ public function test_delete_by_deployment(): void { global $CFG; $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $this->assertTrue($repository->exists($newreslink->get_id())); // Also create a user from this resource link so we get some test user_resource_link mappings. $user = $newreslink->add_user( 2, 'source-id-123', $CFG->lang, 'Perth', 'AU', 'An Example Institution', '99', 2, ); $userrepo = new user_repository(); $userrepo->save($user); global $DB; $this->assertTrue($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); $repository->delete_by_deployment($newreslink->get_deploymentid()); $this->assertFalse($repository->exists($newreslink->get_id())); $this->assertEmpty($repository->find($newreslink->get_id())); $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['resourcelinkid' => $newreslink->get_id()])); $this->assertNull($repository->delete_by_deployment($newreslink->get_deploymentid())); } /** * Test checking existence in the store. * * @covers ::exists */ public function test_exists(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $this->assertTrue($repository->exists($newreslink->get_id())); $repository->delete($newreslink->get_id()); $this->assertFalse($repository->exists($newreslink->get_id())); } /** * Test update of an existing resource_link. * * @covers ::save */ public function test_save_existing(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $newreslink->add_grade_service( new \moodle_url('https://lms.example.org/context/lineitems') ); $updatedreslink = $repository->save($newreslink); $this->assertEquals($newreslink, $updatedreslink); } /** * Test update with a stale object which is no longer present in the store. * * @covers ::save */ public function test_update_stale(): void { $this->resetAfterTest(); $resourcelink = $this->generate_resource_link(); $repository = new resource_link_repository(); $newreslink = $repository->save($resourcelink); $repository->delete($newreslink->get_id()); $newreslink->add_grade_service( new \moodle_url('https://lms.example.org/context/lineitems') ); $this->expectException(\coding_exception::class); $repository->save($newreslink); } } tests/local/ltiadvantage/repository/application_registration_repository_test.php000064400000045137152266447110025050 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\entity\deployment; /** * Tests for the application_registration_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\application_registration_repository */ final class application_registration_repository_test extends \advanced_testcase { /** * Helper to generate a new application_registration object. * * @param string|null $issuer the issuer of the application, or null to use a default. * @param string|null $uniqueid unique id for the tool registration, or null to use a default. * @param string|null $clientid the clientid of the platform's tool registration, or null to use a default. * @return application_registration the application_registration instance. */ protected function generate_application_registration(?string $issuer = null, ?string $uniqueid = null, ?string $clientid = null): application_registration { $issuer = $issuer ?? 'https://lms.example.org'; $uniqueid = $uniqueid ?? 'a2c94a2c94'; $clientid = $clientid ?? 'clientid_123'; return application_registration::create( 'Example LMS application', $uniqueid, new \moodle_url($issuer), $clientid, new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); } /** * Helper to assert that all the key elements of two registrations (i.e. excluding id) are equal. * * @param application_registration $expected the registration whose values are deemed correct. * @param application_registration $check the registration to check. */ protected function assert_same_registration_values(application_registration $expected, application_registration $check): void { $this->assertEquals($expected->get_name(), $check->get_name()); $this->assertEquals($expected->get_platformid(), $check->get_platformid()); $this->assertEquals($expected->get_clientid(), $check->get_clientid()); $this->assertEquals($expected->get_authenticationrequesturl(), $check->get_authenticationrequesturl()); $this->assertEquals($expected->get_jwksurl(), $check->get_jwksurl()); $this->assertEquals($expected->get_accesstokenurl(), $check->get_accesstokenurl()); $this->assertEquals($expected->is_complete(), $check->is_complete()); $this->assertEquals($expected->get_uniqueid(), $check->get_uniqueid()); } /** * Helper to assert that all the key elements of an application_registration are present in the DB. * * @param application_registration $registration */ protected function assert_registration_db_values(application_registration $registration) { global $DB; $record = $DB->get_record('enrol_lti_app_registration', ['id' => $registration->get_id()]); $this->assertEquals($registration->get_id(), $record->id); $this->assertEquals($registration->get_name(), $record->name); $this->assertEquals($registration->get_platformid(), $record->platformid); $this->assertEquals($registration->get_clientid(), $record->clientid); $this->assertEquals($registration->get_authenticationrequesturl(), $record->authenticationrequesturl); $this->assertEquals($registration->get_jwksurl(), $record->jwksurl); $this->assertEquals($registration->get_accesstokenurl(), $record->accesstokenurl); $expectedstatus = $registration->is_complete() ? application_registration::REGISTRATION_STATUS_COMPLETE : application_registration::REGISTRATION_STATUS_INCOMPLETE; $this->assertEquals($expectedstatus, $record->status); $this->assertNotEmpty($record->timecreated); $this->assertNotEmpty($record->timemodified); } /** * Tests saving application_registration instances using the repository. * * @dataProvider save_data_provider * @covers ::save * @param array $registrationdata the registration data */ public function test_save_new(array $registrationdata): void { $this->resetAfterTest(); $reg = application_registration::create_draft($registrationdata['name'], $registrationdata['uniqueid']); if (isset($registrationdata['platformid'])) { $reg->set_platformid($registrationdata['platformid']); } if (isset($registrationdata['clientid'])) { $reg->set_clientid($registrationdata['clientid']); } if (isset($registrationdata['authenticationrequesturl'])) { $reg->set_authenticationrequesturl($registrationdata['authenticationrequesturl']); } if (isset($registrationdata['jwksurl'])) { $reg->set_jwksurl($registrationdata['jwksurl']); } if (isset($registrationdata['accesstokenurl'])) { $reg->set_accesstokenurl($registrationdata['accesstokenurl']); } if (!empty($registrationdata['setcomplete'])) { $reg->complete_registration(); } $repository = new application_registration_repository(); $createdregistration = $repository->save($reg); $this->assert_same_registration_values($reg, $createdregistration); $this->assert_registration_db_values($createdregistration); } /** * Provides registrations in different states for use in test_save_new. * * @return array the array of test data. */ public static function save_data_provider(): array { return [ 'minimal draft' => [ 'registrationdata' => [ 'name' => 'My test platform', 'uniqueid' => 'acbhd4355', ] ], 'draft with only some properties completed' => [ 'registrationdata' => [ 'name' => 'My test platform', 'uniqueid' => 'acbhd4355', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'abc345', ] ], 'draft with all fields completed, not marked complete' => [ 'registrationdata' => [ 'name' => 'My test platform', 'uniqueid' => 'acbhd4355', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'abc345', 'authenticationrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), ] ], 'draft with all fields completed, marked complete' => [ 'registrationdata' => [ 'name' => 'My test platform', 'uniqueid' => 'acbhd4355', 'platformid' => new \moodle_url('https://lms.example.com'), 'clientid' => 'abc345', 'authenticationrequesturl' => new \moodle_url('https://lms.example.com/auth'), 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), 'setcomplete' => true, ] ], ]; } /** * Test saving an application_registration that is already present in the store. * * @covers ::save */ public function test_save_existing(): void { $this->resetAfterTest(); $repository = new application_registration_repository(); // Modifying a draft registration. $draftreg = application_registration::create_draft('My test platform', 'bcvd34gs'); $createddraft = $repository->save($draftreg); $createddraft->set_platformid(new \moodle_url('https://lms.example.com')); $createddraft->set_clientid(new \moodle_url('clientid_test_33333')); $createddraft->set_name('Something else'); $createddraft->set_jwksurl(new \moodle_url('https://lms.example.com/jwks')); $createddraft->set_authenticationrequesturl(new \moodle_url('https://lms.example.com/auth')); $createddraft->set_accesstokenurl(new \moodle_url('https://lms.example.com/token')); $createddraft->complete_registration(); $updateddraft = $repository->save($createddraft); $this->assertEquals($createddraft->get_id(), $updateddraft->get_id()); $this->assert_same_registration_values($createddraft, $updateddraft); $this->assert_registration_db_values($updateddraft); // Modifying a complete registration. $registration = application_registration::create( 'My platform name', 'a2c94a2c94', new \moodle_url('https://updated-lms.example.org/'), 'Updated-client-id', new \moodle_url('https://updated-lms.example.org/auth'), new \moodle_url('https://updated-lms.example.org/jwks'), new \moodle_url('https://updated-lms.example.org/token'), ); $createdregistration = $repository->save($registration); $createdregistration->set_name('Something else'); $createdregistration->set_clientid('hhh444'); $updatedregistration = $repository->save($createdregistration); $this->assertEquals($createdregistration->get_id(), $updatedregistration->get_id()); $this->assert_same_registration_values($createdregistration, $updatedregistration); $this->assert_registration_db_values($updatedregistration); } /** * Tests trying to persist two as-yet-unpersisted objects having identical makeup. * * @covers ::save */ public function test_save_duplicate_unique_constraints(): void { $this->resetAfterTest(); $testregistration = $this->generate_application_registration(); $testregistration2 = $this->generate_application_registration(); $repository = new application_registration_repository(); $this->assertInstanceOf(application_registration::class, $repository->save($testregistration)); $this->expectException(\dml_exception::class); $repository->save($testregistration2); } /** * Test finding an application_registration in the repository. * * @covers ::find */ public function test_find(): void { $this->resetAfterTest(); $testregistration = $this->generate_application_registration(); $repository = new application_registration_repository(); $createdregistration = $repository->save($testregistration); $foundregistration = $repository->find($createdregistration->get_id()); $this->assertEquals($createdregistration->get_id(), $foundregistration->get_id()); $this->assert_same_registration_values($testregistration, $foundregistration); $this->assertNull($repository->find(0)); } /** * Test finding an application registration by its unique id. * * @covers ::find_by_uniqueid */ public function test_find_by_uniqueid(): void { $this->resetAfterTest(); $testregistration = $this->generate_application_registration('https://lms.example.org', 'abc12345'); $repository = new application_registration_repository(); $createdregistration = $repository->save($testregistration); $foundregistration = $repository->find_by_uniqueid($createdregistration->get_uniqueid()); $this->assertEquals($createdregistration->get_id(), $foundregistration->get_id()); $this->assert_same_registration_values($testregistration, $foundregistration); $this->assertNull($repository->find_by_uniqueid('cccc')); } /** * Test finding an application registration by its platform and unique id combination. * * @covers ::find_by_platform_uniqueid */ public function test_find_by_platform_uniqueid(): void { $this->resetAfterTest(); $repository = new application_registration_repository(); $testregistration = $this->generate_application_registration('https://lms.example.org', 'abc12345'); $createdregistration = $repository->save($testregistration); $foundregistration = $repository->find_by_platform_uniqueid($createdregistration->get_platformid(), $createdregistration->get_uniqueid()); $this->assertEquals($createdregistration->get_id(), $foundregistration->get_id()); $this->assert_same_registration_values($testregistration, $foundregistration); // Same platformid, empty uniqueid. $this->assertNull($repository->find_by_platform_uniqueid('https://lms.example.org', '')); // Same platformid, different uniqueid. $this->assertNull($repository->find_by_platform_uniqueid('https://lms.example.org', 'bbbbb')); // Different platformid, empty uniqueid. $this->assertNull($repository->find_by_platform_uniqueid('https://lms-two.example.org', '')); // Different platformid, same uniqueid. $this->assertNull($repository->find_by_platform_uniqueid('https://lms-two.example.org', 'abc12345')); } /** * Test verifying that find_all() returns all registrations. * * @covers ::find_all */ public function test_find_all(): void { $this->resetAfterTest(); // None to begin with. $repository = new application_registration_repository(); $this->assertEquals([], $repository->find_all()); // Add two registrations. $reg1 = $this->generate_application_registration('https://some.platform.org', '123'); $reg2 = $this->generate_application_registration('https://another.platform.org', '456'); $reg1 = $repository->save($reg1); $regns[$reg1->get_id()] = $reg1; $reg2 = $repository->save($reg2); $regns[$reg2->get_id()] = $reg2; // Verify 2 found. $found = $repository->find_all(); $this->assertCount(2, $found); foreach ($found as $reg) { $check = $regns[$reg->get_id()]; $this->assertEquals($check, $reg); } } /** * Test confirming that registrations can be found by their platform string. * * @covers ::find_by_platform */ public function test_find_by_platform(): void { $this->resetAfterTest(); // None to begin with. $repository = new application_registration_repository(); $this->assertNull($repository->find_by_platform('https://some.platform.org', 'abc')); // Create 2 registrations. $reg1 = $this->generate_application_registration('https://some.platform.org', '123', 'abc'); $reg2 = $this->generate_application_registration('https://another.platform.org', '456', 'def'); $reg1 = $repository->save($reg1); $reg2 = $repository->save($reg2); // Verify that we can find the registrations by their platform string. $found = $repository->find_by_platform('https://some.platform.org', 'abc'); $this->assertEquals($reg1, $found); $found2 = $repository->find_by_platform('https://another.platform.org', 'def'); $this->assertEquals($reg2, $found2); } /** * Test checking existence of an application_registration within the repository. * * @covers ::exists */ public function test_exists(): void { $this->resetAfterTest(); $testregistration = $this->generate_application_registration(); $repository = new application_registration_repository(); $createdregistration = $repository->save($testregistration); $this->assertTrue($repository->exists($createdregistration->get_id())); $this->assertFalse($repository->exists(0)); } /** * Test confirming that delete removes items from the repository. * * @covers ::delete */ public function test_delete(): void { $this->resetAfterTest(); global $DB; $reg = $this->generate_application_registration(); $repository = new application_registration_repository(); $reg = $repository->save($reg); $repository->delete($reg->get_id()); $this->assertEquals(0, $DB->count_records('enrol_lti_app_registration')); $this->assertFalse($repository->exists($reg->get_id())); // Deletion of nonexistent registration will not result in errors. $this->assertNull($repository->delete('000000')); } /** * Verify that application registrations can be found through their linked deployments. * * @covers ::find_by_deployment */ public function test_find_by_deployment(): void { $this->resetAfterTest(); $appregrepo = new application_registration_repository(); $deploymentrepo = new deployment_repository(); // Deployment linked to a registration. $testregistration = $this->generate_application_registration(); $createdregistration = $appregrepo->save($testregistration); $deployment1 = $createdregistration->add_tool_deployment('Deployment 1', '12345'); $createddeployment = $deploymentrepo->save($deployment1); // Deployment not linked to a registration. $deployment2 = deployment::create('000', '56789', 'unlinked deployment'); $createddeployment2 = $deploymentrepo->save($deployment2); // Should be able to find the registration from the linked deployment. $foundreg = $appregrepo->find_by_deployment($createddeployment->get_id()); $this->assertEquals($createdregistration, $foundreg); // But not for the deployment which isn't linked. $this->assertNull($appregrepo->find_by_deployment($createddeployment2->get_id())); } } tests/local/ltiadvantage/repository/user_repository_test.php000064400000051733152266447110020730 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\entity\user; /** * Tests for user_repository objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\user_repository */ final class user_repository_test extends \advanced_testcase { /** * Helper to generate a new user instance. * * @param int $mockresourceid used to spoof a published resource, to which this user is associated. * @param array $userfields user information like city, timezone which would normally come from the tool configuration. * @return user a user instance */ protected function generate_user(int $mockresourceid = 1, array $userfields = []): user { global $CFG; $registration = application_registration::create( 'Test', 'a2c94a2c94', new \moodle_url('http://lms.example.org'), 'clientid_123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registrationrepo = new application_registration_repository(); $createdregistration = $registrationrepo->save($registration); $deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123'); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $contextrepo = new context_repository(); $context = $saveddeployment->add_context( 'CTX123', ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'] ); $savedcontext = $contextrepo->save($context); $resourcelinkrepo = new resource_link_repository(); $resourcelink = $saveddeployment->add_resource_link('resourcelinkid_123', $mockresourceid, $savedcontext->get_id()); $savedresourcelink = $resourcelinkrepo->save($resourcelink); // Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth. $user = $this->getDataGenerator()->create_user([ 'city' => '', 'country' => '', 'institution' => '', 'timezone' => '99', 'maildisplay' => 2, 'lang' => 'en' ]); $userdefaultvalues = [ 'lang' => $CFG->lang, 'city' => '', 'country' => '', 'institution' => '', 'timezone' => '99', 'maildisplay' => 2 ]; if (empty($userfields)) { // If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged). $userfields = $userdefaultvalues; } else { // If they have been provided, merge and override the defaults. $userfields = array_merge($userdefaultvalues, $userfields); } $ltiuser = $savedresourcelink->add_user( $user->id, 'source-id-123', ...array_values($userfields) ); $ltiuser->set_lastgrade(67.33333333); return $ltiuser; } /** * Helper to assert that all the key elements of two users (i.e. excluding id) are equal. * * @param user $expected the user whose values are deemed correct. * @param user $check the user to check. * @param bool $checkresourcelink whether or not to confirm the resource link value matches too. */ protected function assert_same_user_values(user $expected, user $check, bool $checkresourcelink = false): void { $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid()); $this->assertEquals($expected->get_city(), $check->get_city()); $this->assertEquals($expected->get_country(), $check->get_country()); $this->assertEquals($expected->get_institution(), $check->get_institution()); $this->assertEquals($expected->get_timezone(), $check->get_timezone()); $this->assertEquals($expected->get_maildisplay(), $check->get_maildisplay()); $this->assertEquals($expected->get_lang(), $check->get_lang()); if ($checkresourcelink) { $this->assertEquals($expected->get_resourcelinkid(), $check->get_resourcelinkid()); } } /** * Helper to assert that all the key elements of a user are present in the DB. * * @param user $expected the user whose values are deemed correct. */ protected function assert_user_db_values(user $expected) { global $DB; $sql = "SELECT u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone, u.maildisplay, u.mnethostid, u.confirmed, u.lang, u.auth FROM {enrol_lti_users} lu JOIN {user} u ON (lu.userid = u.id) WHERE lu.id = :id"; $userrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]); $this->assertEquals($expected->get_city(), $userrecord->city); $this->assertEquals($expected->get_country(), $userrecord->country); $this->assertEquals($expected->get_institution(), $userrecord->institution); $this->assertEquals($expected->get_timezone(), $userrecord->timezone); $this->assertEquals($expected->get_maildisplay(), $userrecord->maildisplay); $this->assertEquals($expected->get_lang(), $userrecord->lang); $ltiuserrecord = $DB->get_record('enrol_lti_users', ['id' => $expected->get_id()]); $this->assertEquals($expected->get_id(), $ltiuserrecord->id); $this->assertEquals($expected->get_sourceid(), $ltiuserrecord->sourceid); $this->assertEquals($expected->get_resourceid(), $ltiuserrecord->toolid); $this->assertEquals($expected->get_lastgrade(), $ltiuserrecord->lastgrade); if ($expected->get_resourcelinkid()) { $sql = "SELECT rl.id FROM {enrol_lti_users} lu JOIN {enrol_lti_user_resource_link} rlj ON (lu.id = rlj.ltiuserid) JOIN {enrol_lti_resource_link} rl ON (rl.id = rlj.resourcelinkid) WHERE lu.id = :id"; $resourcelinkrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]); $this->assertEquals($expected->get_resourcelinkid(), $resourcelinkrecord->id); } } /** * Tests adding a user to the store, assuming that the user has been created using the default 'user default values'. * * @covers ::save */ public function test_save_new_unchanged_user_defaults(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $sink = $this->redirectEvents(); $saveduser = $userrepo->save($user); $events = $sink->get_events(); $sink->close(); $this->assertIsInt($saveduser->get_id()); $this->assert_same_user_values($user, $saveduser, true); $this->assert_user_db_values($saveduser); // No change to underlying user: city, etc. take on default values matching those of the existing user record. $this->assertEmpty($events); } /** * Tests adding a user to the store, assuming that the user has been created using modified 'user default values'. * * @covers ::save */ public function test_save_new_changed_user_defaults(): void { $this->resetAfterTest(); $user = $this->generate_user(1, ['city' => 'Perth']); $userrepo = new user_repository(); $sink = $this->redirectEvents(); $saveduser = $userrepo->save($user); $events = $sink->get_events(); $sink->close(); $this->assertIsInt($saveduser->get_id()); $this->assert_same_user_values($user, $saveduser, true); $this->assert_user_db_values($saveduser); // The underlying user record will change: city ('Perth') differs from that of the existing user (''). $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } /** * Test saving an existing user instance. * * @covers ::save */ public function test_save_existing(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $sink = $this->redirectEvents(); $saveduser = $userrepo->save($user); $events = $sink->get_events(); $sink->close(); $this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged. $saveduser->set_city('New City'); $saveduser->set_country('NZ'); $saveduser->set_lastgrade(99.99999999); $sink = $this->redirectEvents(); $saveduser2 = $userrepo->save($saveduser); $events = $sink->get_events(); $sink->close(); $this->assertEquals($saveduser->get_id(), $saveduser2->get_id()); $this->assert_same_user_values($saveduser, $saveduser2, true); $this->assert_user_db_values($saveduser2); // The underlying user record will change now, since city and country have changed. $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } /** * Test saving an instance which exists by id, but has a different localid to the data in the store. * * @covers ::save */ public function test_save_existing_localid_mismatch(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $user2 = user::create( $saveduser->get_resourceid(), 999999, $saveduser->get_deploymentid(), $saveduser->get_sourceid(), $saveduser->get_lang(), $saveduser->get_timezone(), '', '', '', null, null, null, null, $saveduser->get_id() ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Cannot update user mapping. LTI user '{$saveduser->get_id()}' is already mapped " . "to user '{$saveduser->get_localid()}' and can't be associated with another user '999999'."); $userrepo->save($user2); } /** * Test trying to save a user with an id that is invalid. * * @covers ::save */ public function test_save_stale_id(): void { global $CFG; $this->resetAfterTest(); $instructoruser = $this->getDataGenerator()->create_user(); $userrepo = new user_repository(); $user = user::create( 4, $instructoruser->id, 5, 'source-id-123', $CFG->lang, '99', '', '', '', null, null, null, null, 999999 ); $this->expectException(\coding_exception::class); $this->expectExceptionMessage("Cannot save lti user with id '999999'. The record does not exist."); $userrepo->save($user); } /** * Verify that trying to save a stale object results in an exception referring to unique constraint violation. * * @covers ::save */ public function test_save_uniqueness_constraint(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $userrepo->save($user); $this->expectException(\coding_exception::class); $this->expectExceptionMessageMatches("/Cannot create duplicate LTI user '[a-z0-9_]*' for resource '[0-9]*'/"); $userrepo->save($user); } /** * Test finding a user instance by id. * * @covers ::find */ public function test_find(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $founduser = $userrepo->find($saveduser->get_id()); $this->assertIsInt($founduser->get_id()); $this->assert_same_user_values($saveduser, $founduser, false); $this->assertNull($userrepo->find(0)); } /** * Test finding all of users associated with a given published resource. * * @covers ::find_by_resource */ public function test_find_by_resource(): void { global $CFG; $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $instructoruser = $this->getDataGenerator()->create_user(); $user2 = user::create( $saveduser->get_resourceid(), $instructoruser->id, $saveduser->get_deploymentid(), 'another-user-123', $CFG->lang, '99', 'Perth', 'AU', 'An Example Institution', 2 ); $saveduser2 = $userrepo->save($user2); $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2]; $foundusers = $userrepo->find_by_resource($saveduser->get_resourceid()); $this->assertCount(2, $foundusers); foreach ($foundusers as $founduser) { $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser); } } /** * Test that users can be found based on their resource_link association. * * @covers ::find_by_resource_link */ public function test_find_by_resource_link(): void { global $CFG; $this->resetAfterTest(); $user = $this->generate_user(); $user->set_resourcelinkid(33); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $instructoruser = $this->getDataGenerator()->create_user(); $user2 = user::create( $saveduser->get_resourceid(), $instructoruser->id, $saveduser->get_deploymentid(), 'another-user-123', $CFG->lang, '99', 'Perth', 'AU', 'An Example Institution', 2, null, null, 33 ); $saveduser2 = $userrepo->save($user2); $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2]; $foundusers = $userrepo->find_by_resource_link(33); $this->assertCount(2, $foundusers); foreach ($foundusers as $founduser) { $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser); } } /** * Test checking existence of a user instance, based on id. * * @covers ::exists */ public function test_exists(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $this->assertTrue($userrepo->exists($saveduser->get_id())); $this->assertFalse($userrepo->exists(-50)); } /** * Test deleting a user instance, based on id. * * @covers ::delete */ public function test_delete(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $this->assertTrue($userrepo->exists($saveduser->get_id())); $userrepo->delete($saveduser->get_id()); $this->assertFalse($userrepo->exists($saveduser->get_id())); global $DB; $this->assertFalse($DB->record_exists('enrol_lti_users', ['id' => $saveduser->get_id()])); $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['ltiuserid' => $saveduser->get_id()])); $this->assertTrue($DB->record_exists('user', ['id' => $saveduser->get_localid()])); $this->assertNull($userrepo->delete($saveduser->get_id())); } /** * Test deleting a collection of lti user instances by deployment. * * @covers ::delete_by_deployment */ public function test_delete_by_deployment(): void { global $CFG; $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $instructoruser = $this->getDataGenerator()->create_user(); $instructor2user = $this->getDataGenerator()->create_user(); $user2 = user::create( $saveduser->get_resourceid(), $instructoruser->id, $saveduser->get_deploymentid(), 'another-user-123', $CFG->lang, '99', 'Perth', 'AU', 'An Example Institution', ); $saveduser2 = $userrepo->save($user2); $user3 = user::create( $saveduser->get_resourceid(), $instructor2user->id, $saveduser->get_deploymentid() + 1, 'another-user-678', $CFG->lang, '99', 'Melbourne', 'AU', 'An Example Institution', ); $saveduser3 = $userrepo->save($user3); $this->assertTrue($userrepo->exists($saveduser->get_id())); $this->assertTrue($userrepo->exists($saveduser2->get_id())); $this->assertTrue($userrepo->exists($saveduser3->get_id())); $userrepo->delete_by_deployment($saveduser->get_deploymentid()); $this->assertFalse($userrepo->exists($saveduser->get_id())); $this->assertFalse($userrepo->exists($saveduser2->get_id())); $this->assertTrue($userrepo->exists($saveduser3->get_id())); } /** * Verify a user who has been deleted can be re-saved to the repository and matched to an existing local user. * * @covers ::save */ public function test_save_deleted(): void { $this->resetAfterTest(); $user = $this->generate_user(); $userrepo = new user_repository(); $saveduser = $userrepo->save($user); $userrepo->delete($saveduser->get_id()); $this->assertFalse($userrepo->exists($saveduser->get_id())); $saveduser2 = $userrepo->save($user); $this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid()); $this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id()); } /** * Test confirming that any associated legacy lti user records are not returned by the repository. * * This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users. * * @covers ::find * @covers ::find_single_user_by_resource * @covers ::find_by_resource */ public function test_find_filters_legacy_lti_users(): void { $this->resetAfterTest(); global $DB; $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]); $ltiuserdata = [ 'userid' => $user->id, 'toolid' => $resource->id, 'sourceid' => '1001', ]; $ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata); $userrepo = new user_repository(); $this->assertNull($userrepo->find($ltiuserid)); $this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id)); $this->assertEmpty($userrepo->find_by_resource($resource->id)); // Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned. $ltiuserdata['id'] = $ltiuserid; $ltiuserdata['ltideploymentid'] = '234'; $DB->update_record('enrol_lti_users', $ltiuserdata); $this->assertInstanceOf(user::class, $userrepo->find($ltiuserid)); $this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id)); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $ltiusers); $this->assertInstanceOf(user::class, reset($ltiusers)); } } tests/local/ltiadvantage/repository/context_repository_test.php000064400000021773152266447110021437 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\context; use enrol_lti\local\ltiadvantage\entity\application_registration; /** * Tests for context_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\context_repository */ final class context_repository_test extends \advanced_testcase { /** * Helper to create test context objects for use with the repository tests. * * @return context the context. */ protected function create_test_context(): context { $registration = application_registration::create( 'Test', 'a2c94a2c94', new \moodle_url('http://lms.example.org'), 'clientid_123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registrationrepo = new application_registration_repository(); $createdregistration = $registrationrepo->save($registration); $deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123'); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); return $saveddeployment->add_context('CTX123', ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection']); } /** * Helper to assert that all the key elements of two contexts (i.e. excluding id) are equal. * * @param context $expected the context whose values are deemed correct. * @param context $check the context to check. */ protected function assert_same_context_values(context $expected, context $check): void { $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid()); $this->assertEquals($expected->get_contextid(), $check->get_contextid()); $this->assertEquals($expected->get_types(), $check->get_types()); } /** * Helper to assert that all the key elements of a context are present in the DB. * * @param context $expected the context whose values are deemed correct. */ protected function assert_context_db_values(context $expected) { global $DB; $checkrecord = $DB->get_record('enrol_lti_context', ['id' => $expected->get_id()]); $this->assertEquals($expected->get_id(), $checkrecord->id); $this->assertEquals($expected->get_deploymentid(), $checkrecord->ltideploymentid); $this->assertEquals($expected->get_contextid(), $checkrecord->contextid); $this->assertEquals(json_encode($expected->get_types()), $checkrecord->type); $this->assertNotEmpty($checkrecord->timecreated); $this->assertNotEmpty($checkrecord->timemodified); } /** * Test saving a new context. * * @covers ::save */ public function test_save_new(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $saved = $contextrepo->save($context); $this->assertIsInt($saved->get_id()); $this->assert_same_context_values($context, $saved); $this->assert_context_db_values($saved); } /** * Test saving an existing context. * * @covers ::save */ public function test_save_existing(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $saved = $contextrepo->save($context); $context2 = $context::create( $saved->get_deploymentid(), $saved->get_contextid(), $saved->get_types(), $saved->get_id() ); $saved2 = $contextrepo->save($saved); $this->assertEquals($saved->get_id(), $saved2->get_id()); $this->assert_same_context_values($saved, $saved2); $this->assert_context_db_values($saved2); } /** * Test trying to save two contexts with the same id for the same deployment. * * @covers ::save */ public function test_save_unique_constraints_not_met(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $context2 = clone $context; $contextrepo = new context_repository(); $saved = $contextrepo->save($context); $this->assertInstanceOf(context::class, $saved); $this->expectException(\dml_exception::class); $contextrepo->save($context2); } /** * Test existence of a context within the repository. * * @covers ::exists */ public function test_exists(): void { $this->resetAfterTest(); $contextrepo = new context_repository(); $context = $this->create_test_context(); $savedcontext = $contextrepo->save($context); $this->assertTrue($contextrepo->exists($savedcontext->get_id())); $this->assertFalse($contextrepo->exists(0)); } /** * Test finding a context in the repository. * * @covers ::find */ public function test_find(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $savedcontext = $contextrepo->save($context); $foundcontext = $contextrepo->find($savedcontext->get_id()); $this->assertEquals($savedcontext->get_id(), $foundcontext->get_id()); $this->assert_same_context_values($savedcontext, $foundcontext); $this->assertNull($contextrepo->find(0)); } /** * Test finding a context by contextid within the deployment. * * @covers ::find_by_contextid */ public function test_find_by_contextid(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $savedcontext = $contextrepo->save($context); $foundcontext = $contextrepo->find_by_contextid($savedcontext->get_contextid(), $savedcontext->get_deploymentid()); $this->assertEquals($savedcontext->get_id(), $foundcontext->get_id()); $this->assert_same_context_values($savedcontext, $foundcontext); $this->assertNull($contextrepo->find_by_contextid(0, $savedcontext->get_deploymentid())); } /** * Test deleting a context from the repository. * * @covers ::delete */ public function test_delete(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $savedcontext = $contextrepo->save($context); $this->assertTrue($contextrepo->exists($savedcontext->get_id())); $contextrepo->delete($savedcontext->get_id()); $this->assertFalse($contextrepo->exists($savedcontext->get_id())); $this->assertNull($contextrepo->delete($savedcontext->get_id())); } /** * Test deleting a context from the repository, by deployment. * * @covers ::delete_by_deployment */ public function test_delete_by_deployment(): void { $this->resetAfterTest(); $context = $this->create_test_context(); $contextrepo = new context_repository(); $savedcontext = $contextrepo->save($context); $context2 = context::create($savedcontext->get_deploymentid(), 'new-context-345', ['CourseSection']); $savedcontext2 = $contextrepo->save($context2); $context3 = context::create($savedcontext->get_deploymentid() + 1, 'new-context-567', ['CourseSection']); $savedcontext3 = $contextrepo->save($context3); $this->assertTrue($contextrepo->exists($savedcontext->get_id())); $this->assertTrue($contextrepo->exists($savedcontext2->get_id())); $this->assertTrue($contextrepo->exists($savedcontext3->get_id())); $contextrepo->delete_by_deployment($savedcontext->get_deploymentid()); $this->assertFalse($contextrepo->exists($savedcontext->get_id())); $this->assertFalse($contextrepo->exists($savedcontext2->get_id())); $this->assertTrue($contextrepo->exists($savedcontext3->get_id())); $this->assertNull($contextrepo->delete_by_deployment($savedcontext->get_id())); } } tests/local/ltiadvantage/repository/legacy_consumer_repository_test.php000064400000005175152266447110023130 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for legacy_consumer_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository */ final class legacy_consumer_repository_test extends \lti_advantage_testcase { /** * Test the get_consumer_secrets repository method. * * @covers ::get_consumer_secrets */ public function test_get_consumer_secrets(): void { $this->resetAfterTest(); // Set up legacy consumer information. $course = $this->getDataGenerator()->create_course(); // Note below that 2 tools with the same secret have been used (three tools total) // but we expect only the distinct secrets to be returned. $legacydata = [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ]; [$tools, $consumer, $users] = $this->setup_legacy_data($course, $legacydata); $legacyconsumerrepo = new legacy_consumer_repository(); // Find the tool secrets associated with 'CONSUMER_1'. $consumersecrets = $legacyconsumerrepo->get_consumer_secrets('CONSUMER_1'); $this->assertCount(2, $consumersecrets); foreach ($consumersecrets as $consumersecret) { $this->assertContains($consumersecret, ['toolsecret1', 'toolsecret2']); } // Verify an empty array is returned for a non-existent consumer. $this->assertEmpty($legacyconsumerrepo->get_consumer_secrets('CONSUMER_2')); } } tests/local/ltiadvantage/repository/published_resource_repository_test.php000064400000022713152266447110023634 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\helper; /** * Tests for published_resource_repository objects. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\published_resource_repository */ final class published_resource_repository_test extends \advanced_testcase { /** * Get a list of published resources for testing. * * @return array a list of relevant test data; users courses and mods. */ protected function generate_published_resources() { // Create a course and publish it. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $course2 = $generator->create_course(); $user = $generator->create_and_enrol($course, 'editingteacher'); $user2 = $generator->create_and_enrol($course2, 'editingteacher'); $user3 = $generator->create_user(); $this->setAdminUser(); $mod = $generator->create_module('assign', ['course' => $course->id]); $mod2 = $generator->create_module('resource', ['course' => $course2->id]); $mod3 = $generator->create_module('assign', ['course' => $course->id, 'grade' => 0]); $mod4 = $generator->create_module('workshop', ['course' => $course->id]); // Had multiple grade items. $courseresourcedata = (object) [ 'courseid' => $course->id, 'membersyncmode' => 0, 'membersync' => 0, 'ltiversion' => 'LTI-1p3' ]; $moduleresourcedata = (object) [ 'courseid' => $course->id, 'cmid' => $mod->cmid, 'membersyncmode' => 1, 'membersync' => 1, 'ltiversion' => 'LTI-1p3' ]; $module2resourcedata = (object) [ 'courseid' => $course2->id, 'cmid' => $mod2->cmid, 'membersyncmode' => 1, 'membersync' => 1, 'ltiversion' => 'LTI-1p3' ]; $module3resourcedata = (object) [ 'courseid' => $course->id, 'cmid' => $mod3->cmid, 'membersyncmode' => 1, 'membersync' => 1, 'ltiversion' => 'LTI-1p3' ]; $module4resourcedata = (object) [ 'courseid' => $course->id, 'cmid' => $mod4->cmid, 'membersyncmode' => 1, 'membersync' => 1, 'ltiversion' => 'LTI-1p3' ]; $coursetool = $generator->create_lti_tool($courseresourcedata); $coursetool = helper::get_lti_tool($coursetool->id); $modtool = $generator->create_lti_tool($moduleresourcedata); $modtool = helper::get_lti_tool($modtool->id); $mod2tool = $generator->create_lti_tool($module2resourcedata); $mod2tool = helper::get_lti_tool($mod2tool->id); $mod3tool = $generator->create_lti_tool($module3resourcedata); $mod3tool = helper::get_lti_tool($mod3tool->id); $mod4tool = $generator->create_lti_tool($module4resourcedata); $mod4tool = helper::get_lti_tool($mod4tool->id); return [$user, $user2, $user3, $course, $course2, $mod, $mod2, $mod3, $mod4, $coursetool, $modtool, $mod2tool, $mod3tool, $mod4tool]; } /** * Test finding published resources for a given user. * * @covers ::find_all_for_user */ public function test_find_all_for_user(): void { $this->resetAfterTest(); [$user, $user2, $user3, $course, $course2, $mod, $mod2, $mod3, $mod4] = $this->generate_published_resources(); $resourcerepo = new published_resource_repository(); $resources = $resourcerepo->find_all_for_user($user->id); $this->assertCount(4, $resources); usort($resources, function($a, $b) { return strcmp($a->get_contextid(), $b->get_contextid()); }); $this->assertEquals($resources[0]->get_contextid(), \context_course::instance($course->id)->id); $this->assertEquals($resources[1]->get_contextid(), \context_module::instance($mod->cmid)->id); $this->assertEquals($resources[2]->get_contextid(), \context_module::instance($mod3->cmid)->id); $this->assertEquals($resources[3]->get_contextid(), \context_module::instance($mod4->cmid)->id); $this->assertTrue($resources[0]->supports_grades()); $this->assertTrue($resources[1]->supports_grades()); $this->assertFalse($resources[2]->supports_grades()); $this->assertFalse($resources[3]->supports_grades()); // Multiple grade items isn't supported in content selection. $resources = $resourcerepo->find_all_for_user($user2->id); $this->assertCount(1, $resources); $this->assertEquals($resources[0]->get_contextid(), \context_module::instance($mod2->cmid)->id); $this->assertFalse($resources[0]->supports_grades()); $this->assertEmpty($resourcerepo->find_all_for_user($user3->id)); } /** * Test finding a subset of published resources, by id, for a user. * * @covers ::find_all_by_ids_for_user */ public function test_find_all_by_ids_for_user(): void { $this->resetAfterTest(); [$user, $user2, $user3, $course, $course2, $mod, $mod2, $mod3, $mod4, $coursetool, $tool, $tool2, $tool3, $tool4] = $this->generate_published_resources(); $resourcerepo = new published_resource_repository(); $resources = $resourcerepo->find_all_by_ids_for_user([$tool->id, $tool2->id, $tool3->id, $tool4->id], $user->id); $this->assertCount(3, $resources); usort($resources, function ($a, $b) { return strcmp($a->get_contextid(), $b->get_contextid()); }); $this->assertEquals($resources[0]->get_contextid(), \context_module::instance($mod->cmid)->id); $this->assertEquals($resources[1]->get_contextid(), \context_module::instance($mod3->cmid)->id); $this->assertEquals($resources[2]->get_contextid(), \context_module::instance($mod4->cmid)->id); $this->assertTrue($resources[0]->supports_grades()); $this->assertFalse($resources[1]->supports_grades()); $this->assertFalse($resources[2]->supports_grades()); // Multiple grade items isn't supported in content selection. $resources = $resourcerepo->find_all_by_ids_for_user([$tool->id, $tool2->id, $tool3->id], $user2->id); $this->assertCount(1, $resources); $this->assertEquals($resources[0]->get_contextid(), \context_module::instance($mod2->cmid)->id); $this->assertFalse($resources[0]->supports_grades()); $this->assertEmpty($resourcerepo->find_all_by_ids_for_user([$tool->id], $user2->id)); $this->assertEmpty($resourcerepo->find_all_by_ids_for_user([], $user2->id)); } /** * Test finding published resources for different roles having different capabilities at the course level. * * @covers ::find_all_for_user */ public function test_find_all_for_user_no_permissions(): void { $this->resetAfterTest(); global $DB; [$user, $user2, $user3, $course, $course2, $mod, $mod2, $mod3, $mod4, $coursetool, $modtool, $mod2tool] = $this->generate_published_resources(); // Grant the user permissions as an editing teacher in a specific module within the course, // as if the user had launched into the module via LTI enrolment, with an instructor role of 'editingteacher'. $modaccessonlyuser = $this->getDataGenerator()->create_user(); helper::enrol_user($modtool, $modaccessonlyuser->id); // Enrolment only, no role. $editingteacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); role_assign($editingteacherrole->id, $modaccessonlyuser->id, $modtool->contextid); // Verify that without a course role of editing teacher (not module role), the published content isn't visible. $resourcerepo = new published_resource_repository(); $resources = $resourcerepo->find_all_for_user($modaccessonlyuser->id); $this->assertCount(0, $resources); // Now, give the user a course role of 'editingteacher' and confirm they can see the published content. $this->getDataGenerator()->enrol_user($modaccessonlyuser->id, $course->id, 'editingteacher'); $resources = $resourcerepo->find_all_for_user($modaccessonlyuser->id); $this->assertCount(4, $resources); // Check other course level roles without the capability, e.g. 'teacher'. role_unassign($editingteacherrole->id, $modaccessonlyuser->id, \context_course::instance($course->id)->id); $this->getDataGenerator()->enrol_user($modaccessonlyuser->id, $course->id, 'teacher'); $resources = $resourcerepo->find_all_for_user($modaccessonlyuser->id); $this->assertCount(0, $resources); } } tests/local/ltiadvantage/repository/deployment_repository_test.php000064400000033777152266447110022142 0ustar00. namespace enrol_lti\local\ltiadvantage\repository; use enrol_lti\local\ltiadvantage\entity\application_registration; use enrol_lti\local\ltiadvantage\entity\deployment; /** * Tests for deployment_repository. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\deployment_repository */ final class deployment_repository_test extends \advanced_testcase { /** * Helper to create test deployment objects for use with the repository tests. * * @param string $deploymentid the string id of the deployment. * @param int|null $appregistrationid the id of the application registration to which this deployment belongs. * @return deployment the deployment. */ protected function create_test_deployment(string $deploymentid = 'DeployID123', ?int $appregistrationid = null): deployment { if (is_null($appregistrationid)) { $registration = application_registration::create( 'Test', 'a2c94a2c94', new \moodle_url('http://lms.example.org'), 'clientid_123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registrationrepo = new application_registration_repository(); $createdregistration = $registrationrepo->save($registration); $appregistrationid = $createdregistration->get_id(); } return deployment::create( $appregistrationid, $deploymentid, 'Tool deployment on platform x', ); } /** * Helper to assert that all the key elements of two deployments (i.e. excluding id) are equal. * * @param deployment $expected the deployment whose values are deemed correct. * @param deployment $check the deployment to check. */ protected function assert_same_deployment_values(deployment $expected, deployment $check): void { $this->assertEquals($expected->get_deploymentname(), $check->get_deploymentname()); $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid()); $this->assertEquals($expected->get_registrationid(), $check->get_registrationid()); $this->assertEquals($expected->get_legacy_consumer_key(), $check->get_legacy_consumer_key()); } /** * Helper to assert that all the key elements of a deployment are present in the DB. * * @param deployment $expected the deployment whose values are deemed correct. */ protected function assert_deployment_db_values(deployment $expected) { global $DB; $checkrecord = $DB->get_record('enrol_lti_deployment', ['id' => $expected->get_id()]); $this->assertEquals($expected->get_id(), $checkrecord->id); $this->assertEquals($expected->get_deploymentname(), $checkrecord->name); $this->assertEquals($expected->get_deploymentid(), $checkrecord->deploymentid); $this->assertEquals($expected->get_registrationid(), $checkrecord->platformid); $this->assertEquals($expected->get_legacy_consumer_key(), $checkrecord->legacyconsumerkey); $this->assertNotEmpty($checkrecord->timecreated); $this->assertNotEmpty($checkrecord->timemodified); } /** * Test saving a new deployment. * * @covers ::save */ public function test_save_new(): void { $this->resetAfterTest(); $deploymentrepo = new deployment_repository(); $deployment = $this->create_test_deployment(); $deployment->set_legacy_consumer_key('test-consumer-key'); $saved = $deploymentrepo->save($deployment); $this->assertIsInt($saved->get_id()); $this->assert_same_deployment_values($deployment, $saved); $this->assert_deployment_db_values($saved); } /** * Test saving an existing deployment. * * @covers ::save */ public function test_save_existing(): void { $this->resetAfterTest(); $deploymentrepo = new deployment_repository(); $deployment = $this->create_test_deployment(); $saved = $deploymentrepo->save($deployment); $saved->set_legacy_consumer_key('added-consumer-key'); $saved2 = $deploymentrepo->save($saved); $this->assertEquals($saved->get_id(), $saved2->get_id()); $this->assert_same_deployment_values($saved, $saved2); $this->assert_deployment_db_values($saved2); } /** * Test trying to save two deployments of identical nature in sequence. * * @covers ::save */ public function test_save_unique_constraints_not_met(): void { $this->resetAfterTest(); $deployment1 = $this->create_test_deployment('Deploy_ID_123'); $deployment2 = $this->create_test_deployment('Deploy_ID_123', $deployment1->get_registrationid()); $deploymentrepo = new deployment_repository(); $this->assertInstanceOf(deployment::class, $deploymentrepo->save($deployment1)); $this->expectException(\dml_exception::class); $deploymentrepo->save($deployment2); } /** * Test existence of a deployment within the repository. * * @covers ::exists */ public function test_exists(): void { $this->resetAfterTest(); $deploymentrepo = new deployment_repository(); $deployment = $this->create_test_deployment(); $saveddeployment = $deploymentrepo->save($deployment); $this->assertTrue($deploymentrepo->exists($saveddeployment->get_id())); $this->assertFalse($deploymentrepo->exists(0)); } /** * Test finding a deployment in the repository. * * @covers ::find */ public function test_find(): void { $this->resetAfterTest(); $deployment = $this->create_test_deployment(); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $founddeployment = $deploymentrepo->find($saveddeployment->get_id()); $this->assertEquals($saveddeployment->get_id(), $founddeployment->get_id()); $this->assert_same_deployment_values($saveddeployment, $founddeployment); $this->assertNull($deploymentrepo->find(0)); } /** * Test deleting a deployment object from the repository. * * @covers ::delete */ public function test_delete(): void { $this->resetAfterTest(); $deployment = $this->create_test_deployment(); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $this->assertTrue($deploymentrepo->exists($saveddeployment->get_id())); $deploymentrepo->delete($saveddeployment->get_id()); $this->assertFalse($deploymentrepo->exists($saveddeployment->get_id())); $this->assertNull($deploymentrepo->delete($saveddeployment->get_id())); } /** * Test deleting a deployment by registration. * * @covers ::delete_by_registration */ public function test_delete_by_registration(): void { $this->resetAfterTest(); $deployment = $this->create_test_deployment(); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $deployment2 = deployment::create($saveddeployment->get_registrationid(), 'another-deployment-id-1', 'another deployment 1'); $saveddeployment2 = $deploymentrepo->save($deployment2); $deployment3 = deployment::create($saveddeployment->get_registrationid() + 1, 'another-deployment-id-2', 'another deployment 2'); $saveddeployment3 = $deploymentrepo->save($deployment3); $this->assertTrue($deploymentrepo->exists($saveddeployment->get_id())); $this->assertTrue($deploymentrepo->exists($saveddeployment2->get_id())); $this->assertTrue($deploymentrepo->exists($saveddeployment3->get_id())); $deploymentrepo->delete_by_registration($saveddeployment->get_registrationid()); $this->assertFalse($deploymentrepo->exists($saveddeployment->get_id())); $this->assertFalse($deploymentrepo->exists($saveddeployment2->get_id())); $this->assertTrue($deploymentrepo->exists($saveddeployment3->get_id())); $this->assertNull($deploymentrepo->delete($saveddeployment->get_id())); } /** * Test counting the number of deployments for a given registration. * * @covers ::count_by_registration */ public function test_count_by_registration(): void { $this->resetAfterTest(); $deployment = $this->create_test_deployment(); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $deployment2 = deployment::create($saveddeployment->get_registrationid(), 'another-deployment-id-1', 'another deployment 1'); $saveddeployment2 = $deploymentrepo->save($deployment2); $deployment3 = deployment::create($saveddeployment->get_registrationid() + 1, 'another-deployment-id-2', 'another deployment 2'); $saveddeployment3 = $deploymentrepo->save($deployment3); $this->assertEquals(2, $deploymentrepo->count_by_registration($saveddeployment->get_registrationid())); $this->assertEquals(1, $deploymentrepo->count_by_registration($saveddeployment3->get_registrationid())); $this->assertEquals(0, $deploymentrepo->count_by_registration(0)); } /** * Test confirming a deployment can be found by registration and deploymentid. * * @covers ::find_by_registration */ public function test_find_by_registration(): void { $this->resetAfterTest(); $deployment = $this->create_test_deployment(); $deploymentrepo = new deployment_repository(); $saveddeployment = $deploymentrepo->save($deployment); $regid = $saveddeployment->get_registrationid(); // Existing registration. $this->assertInstanceOf(deployment::class, $deploymentrepo->find_by_registration($regid, $saveddeployment->get_deploymentid())); // A non-existent registration. $this->assertNull($deploymentrepo->find_by_registration($regid, 'NonExistentDeploymentId')); } /** * Testing that all deployments for a given registration can be fetched. * * @covers ::find_all_by_registration */ public function test_find_all_by_registration(): void { $this->resetAfterTest(); $registration1 = application_registration::create( 'Test', 'a2c94a2c94', new \moodle_url('http://lms.example.org'), 'clientid_123', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registration2 = application_registration::create( 'Test 2', 'c56bcdc56bcd', new \moodle_url('http://lms2.example.org'), 'clientid_345', new \moodle_url('https://example.org/authrequesturl'), new \moodle_url('https://example.org/jwksurl'), new \moodle_url('https://example.org/accesstokenurl') ); $registrationrepo = new application_registration_repository(); $deploymentrepo = new deployment_repository(); $createdregistration1 = $registrationrepo->save($registration1); $createdregistration2 = $registrationrepo->save($registration2); $deployment1 = $createdregistration1->add_tool_deployment('Deployment 1', 'reg1_deploy1'); $deployment2 = $createdregistration1->add_tool_deployment('Deployment 2', 'reg1_deploy2'); $deployment3 = $createdregistration2->add_tool_deployment('Deployment 3', 'reg2_deploy1'); $saveddeployment1 = $deploymentrepo->save($deployment1); $saveddeployment2 = $deploymentrepo->save($deployment2); $saveddeployment3 = $deploymentrepo->save($deployment3); $reg1saveddeployments = [ $saveddeployment1->get_id() => $saveddeployment1, $saveddeployment2->get_id() => $saveddeployment2 ]; $reg2saveddeployments = [ $saveddeployment3->get_id() => $saveddeployment3 ]; // Registration 1. $reg1founddeployments = $deploymentrepo->find_all_by_registration($createdregistration1->get_id()); $this->assertCount(2, $reg1founddeployments); foreach ($reg1founddeployments as $reg1founddeployment) { $this->assertEquals($reg1saveddeployments[$reg1founddeployment->get_id()], $reg1founddeployment); } // Registration 2. $reg2founddeployments = $deploymentrepo->find_all_by_registration($createdregistration2->get_id()); $this->assertCount(1, $deploymentrepo->find_all_by_registration($createdregistration2->get_id())); foreach ($reg2founddeployments as $reg2founddeployment) { $this->assertEquals($reg2saveddeployments[$reg2founddeployment->get_id()], $reg2founddeployment); } // A non-existent registration. $nonexistentregdeployments = $deploymentrepo->find_all_by_registration(0); $this->assertEmpty($nonexistentregdeployments); } } tests/local/ltiadvantage/task/sync_tool_grades_test.php000064400000141515152266447110017532 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use enrol_lti\helper; use Packback\Lti1p3\LtiAssignmentsGradesService; use Packback\Lti1p3\LtiGrade; use Packback\Lti1p3\LtiLineitem; use core\task\manager; use phpunit_util; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the enrol_lti\local\ltiadvantage\task\sync_tool_grades adhoc task. * * @package enrol_lti * @copyright 2023 David Pesce * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_tool_grades */ final class sync_tool_grades_test extends \lti_advantage_testcase { /** * Get a task which has a mocked ags instance injected, meaning no real calls will be made to the platform. * * This allows us to test the behaviour of the task (in terms of which users are in scope and which grades are sent) * without needing to deal with any auth. * * @param string $statuscode the HTTP status code to simulate. * @param bool $mockexception whether to simulate an exception during the service call or not. * @return sync_grades instance of the task with a mocked ags instance inside. */ protected function get_task_with_mocked_grade_service($statuscode = '200', $mockexception = false): sync_tool_grades { $mockgradeservice = $this->getMockBuilder(LtiAssignmentsGradesService::class) ->disableOriginalConstructor() ->onlyMethods(['putGrade']) ->getMock(); $mockgradeservice->method('putGrade')->willReturnCallback(function() use ($statuscode, $mockexception) { if ($mockexception) { throw new \Exception(); } return ['headers' => ['httpstatus' => "HTTP/2 $statuscode OK"], 'body' => '', 'status' => $statuscode]; }); // Get a mock task, with the method 'get_ags()' mocked to return the mocked AGS instance. $mocktask = $this->getMockBuilder(sync_tool_grades::class) ->onlyMethods(['get_ags']) ->getMock(); $mocktask->method('get_ags')->willReturn($mockgradeservice); return $mocktask; } /** * Helper function to set a grade for a user. * * @param int $userid the id of the user being graded. * @param float $grade the grade value, out of 100, to set for the user. * @param \stdClass $resource the published resource object. * @return float the fractional grade value expected to be used during sync. */ protected function set_user_grade_for_resource(int $userid, float $grade, \stdClass $resource): float { global $CFG; require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/gradelib.php'); $context = \context::instance_by_id($resource->contextid); if ($context->contextlevel == CONTEXT_COURSE) { $gi = \grade_item::fetch_course_item($resource->courseid); } else if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id('assign', $context->instanceid); $gi = \grade_item::fetch([ 'itemtype' => 'mod', 'itemmodule' => 'assign', 'iteminstance' => $cm->instance, 'courseid' => $resource->courseid ]); } if ($ggrade = \grade_grade::fetch(['itemid' => $gi->id, 'userid' => $userid])) { $ggrade->finalgrade = $grade; $ggrade->rawgrade = $grade; $ggrade->update(); } else { $ggrade = new \grade_grade(); $ggrade->itemid = $gi->id; $ggrade->userid = $userid; $ggrade->rawgrade = $grade; $ggrade->finalgrade = $grade; $ggrade->rawgrademax = 100; $ggrade->rawgrademin = 0; $ggrade->timecreated = time(); $ggrade->timemodified = time(); $ggrade->insert(); } return floatval($ggrade->finalgrade / $gi->grademax); } /** * Helper to set the completion status for published course or module. * * @param \stdClass $resource the resource - either a course or module. * @param int $userid the id of the user to override the completion status for. * @param bool $complete whether the resource is deemed complete or not. */ protected function override_resource_completion_status_for_user(\stdClass $resource, int $userid, bool $complete): void { global $CFG; require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/datalib.php'); $this->setAdminUser(); $context = \context::instance_by_id($resource->contextid); $completion = new \completion_info(get_course($resource->courseid)); if ($context->contextlevel == CONTEXT_COURSE) { $ccompletion = new \completion_completion(['userid' => $userid, 'course' => $resource->courseid]); if ($complete) { $ccompletion->mark_complete(); } else { $completion->clear_criteria(); } } else if ($context->contextlevel == CONTEXT_MODULE) { $completion->update_state( get_coursemodule_from_id('assign', $context->instanceid), $complete ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE, $userid, true ); } } /** * Data provider for test_grade_sync_positive_case. * * @return array */ public static function grade_sync_positive_cases(): array { return [ [200], [201], [202], [204], ]; } /** * Test the sync grades task works correct when platform responses with given status code. * * @covers ::execute * @param string $statuscode the response status code with which the job should work correctly * @dataProvider grade_sync_positive_cases */ public function test_grade_sync_positive_case($statuscode): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); $task = $this->get_task_with_mocked_grade_service($statuscode); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); $student1user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); // Grade student1. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Sync and verify that only student1's grade is sent. ob_start(); $task->set_custom_data($resource); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 2 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test the sync grades task during several runs and for a series of grade changes. * * @covers ::execute */ public function test_grade_sync_chronological_syncs(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); $task = $this->get_task_with_mocked_grade_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); // Grade student1 only. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Sync and verify that only student1's grade is sent. ob_start(); $task->set_custom_data($resource); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Sync again, verifying no grades are sent because nothing has changed. ob_start(); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ". "'$course->id' was not sent as the grades are the same.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 0 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Change student1's grade and add a grade for student2. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource); $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource); // Sync again, verifying both grade changes are sent. ob_start(); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 2 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test a grade sync when there are more than one resource link for the resource. * * @covers ::execute */ public function test_grade_sync_multiple_resource_links(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); $task = $this->get_task_with_mocked_grade_service(); // Launch the resource first for an instructor using the default resource link in the platform. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch again as the instructor, this time from a different resource link in the platform. $teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2'); $launchservice->user_launches_tool($instructoruser, $teachermocklaunch2); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]); $student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]); $student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2'); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch); $launchservice->user_launches_tool($student1user, $student1reslink2launch); // Grade student1 only. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links. ob_start(); $task->set_custom_data($resource); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Found 2 resource link(s) for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Verify that the grade was reported as being synced twice - once for each resource link. $expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n". "Processing resource link '.*'.\n". "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.\n". "Processing resource link '.*'.\n". "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent./"; $this->assertMatchesRegularExpression($expected, $ob); } /** * Test the grade sync task when the launch data doesn't include the AGS support. * * @covers ::execute */ public function test_sync_grades_no_service_endpoint(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, null); $instructoruser = $this->getDataGenerator()->create_user(); [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); $task = $this->get_task_with_mocked_grade_service(); $task->set_custom_data($resource); $this->expectOutputRegex( "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". "course '$course->id'. Attempting to sync grades for all.\n". "Processing resource link '.*'.\n". "Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ". "course '$course->id'.\n". "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". "sent 0 grades./" ); $task->execute(); } /** * Test syncing grades when the enrolment instance is disabled. * * @covers ::execute */ public function test_sync_grades_disabled_instance(): void { $this->resetAfterTest(); global $DB; [$course, $resource, $resource2, $resource3] = $this->create_test_environment(); // Disable resource 1. $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED]; $DB->update_record('enrol', $enrol); // Delete the activity being shared by resource 2, leaving resource 2 disabled as a result. $modcontext = \context::instance_by_id($resource2->contextid); course_delete_module($modcontext->instanceid); // Only the enabled resource 3 should sync grades. $task = $this->get_task_with_mocked_grade_service(); $task->set_custom_data($resource3); $this->expectOutputRegex( "/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n". "Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ". "sent 0 grades.\n$/" ); $task->execute(); } /** * Test the grade sync when the context has been deleted in between launch and when the grade sync task is run. * * @covers ::execute */ public function test_sync_grades_deleted_context(): void { $this->resetAfterTest(); global $DB; [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Delete the activity, then enable the enrolment method (it is disabled during activity deletion). $modcontext = \context::instance_by_id($resource->contextid); course_delete_module($modcontext->instanceid); $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED]; $DB->update_record('enrol', $enrol); $task = $this->get_task_with_mocked_grade_service(); $task->set_custom_data($resource); $this->expectOutputRegex( "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". "course '$course->id'. Attempting to sync grades for all.\n". "Processing resource link '.*'.\n". "Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n". "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". "sent 0 grades./" ); $task->execute(); } /** * Test grade sync when completion is required for the activity before sync takes place. * * @covers ::execute */ public function test_sync_grades_completion_required(): void { $this->resetAfterTest(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); [ $course, $resource, $resource2, $publishedcourse ] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true); $launchservice = $this->get_tool_launch_service(); $task = $this->get_task_with_mocked_grade_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); // Launch the published course as student2. $student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456'); $launchservice->user_launches_tool($student2user, $student2mockcourselaunch); // Grade student1 in the assign resource. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // And student2 in the course resource. $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse); // Since adhoc tasks are queued via sync_grades scheduled task, we need to create a queue. $allitems = array($resource, $resource2, $publishedcourse); // Sync and verify that no grades are sent because resource and published course are both not yet complete. ob_start(); foreach ($allitems as $item) { $task->set_custom_data($item); $task->execute(); } $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". "the course '$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 0 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Complete the resource for student1. $this->override_resource_completion_status_for_user($resource, $student1id, true); // Run the sync again, this time confirming the grade for student1 is sent. ob_start(); foreach ($allitems as $item) { $task->set_custom_data($item); $task->execute(); } $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades.", "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". "the course '$course->id'.", "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". "Processed 1 users; sent 0 grades.", ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Fail completion for student1 and confirm no grade is sent, even despite it being changed. $this->set_user_grade_for_resource($student1id, 33.3, $resource); $this->override_resource_completion_status_for_user($resource, $student1id, false); ob_start(); foreach ($allitems as $item) { $task->set_custom_data($item); $task->execute(); } $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 0 grades.", "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". "the course '$course->id'.", "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". "Processed 1 users; sent 0 grades.", ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Complete the course for student2 and verify the grade is now sent. $this->override_resource_completion_status_for_user($publishedcourse, $student2id, true); ob_start(); foreach ($allitems as $item) { $task->set_custom_data($item); $task->execute(); } $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 0 grades.", "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". "'$publishedcourse->id' and the course '$course->id' was sent.", "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". "Processed 1 users; sent 1 grades.", ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Mark the course as in progress again for student2 and verify any new grade changes are not sent. $this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse); $this->override_resource_completion_status_for_user($publishedcourse, $student2id, false); ob_start(); foreach ($allitems as $item) { $task->set_custom_data($item); $task->execute(); } $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". "'$course->id'.", "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". "the course '$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 0 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code. * * @covers ::execute */ public function test_sync_grades_failed_service_call(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); $task = $this->get_task_with_mocked_grade_service('200', true); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a student, creating the enrolment and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); $student1user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); // Grade student1 in the assign resource. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes. ob_start(); $task->set_custom_data($resource); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' failed to send.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 2 users; sent 0 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } // Now run the sync again, this time with a bad http response code. $task = $this->get_task_with_mocked_grade_service('400'); ob_start(); $task->set_custom_data($resource); $task->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' failed to send.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 2 users; sent 0 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected. * * @covers ::execute */ public function test_sync_grades_coupled_lineitem(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); // The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried. $agsclaim = [ "scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"], "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" ]; // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, $agsclaim); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); // Grade student1 only. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object. $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); $mockgradeservice->method('putGrade')->willReturnCallback(function() { return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; }); $mockgradeservice->expects($this->never()) ->method('findOrCreateLineitem'); $mockgradeservice->expects($this->once()) ->method('putGrade') ->with($this->isInstanceOf(LtiGrade::class)); $mocktask = $this->getMockBuilder(sync_tool_grades::class) ->onlyMethods(['get_ags']) ->getMock(); $mocktask->method('get_ags')->willReturn($mockgradeservice); // Sync and verify that only student1's grade is sent. ob_start(); $mocktask->set_custom_data($resource); $mocktask->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test the sync for an activity context when only the lineitems URL is provided and when line item creation/query is expected. * * @covers ::execute */ public function test_sync_grades_none_or_many_lineitems_activity_context(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items. $agsclaim = [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", ], "lineitems" => "https://platform.example.com/10/lineitems" ]; // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, $agsclaim); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); // Grade student1 only. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Mock task, asserting that line item creation takes place via a mock grade service object. $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); $mockgradeservice->method('putGrade')->willReturnCallback(function() { return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; }); $mockgradeservice->expects($this->once()) ->method('findOrCreateLineitem'); $mockgradeservice->expects($this->once()) ->method('putGrade') ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class)); $mocktask = $this->getMockBuilder(sync_tool_grades::class) ->onlyMethods(['get_ags']) ->getMock(); $mocktask->method('get_ags')->willReturn($mockgradeservice); // Sync and verify that only student1's grade is sent. ob_start(); $mocktask->set_custom_data($resource); $mocktask->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } /** * Test the sync for a course context when only the lineitems URL is provided and when line item creation/query is expected. * * @covers ::execute */ public function test_sync_grades_none_or_many_lineitems_course_context(): void { $this->resetAfterTest(); [$course, $tool1, $tool2, $resource] = $this->create_test_environment(); $launchservice = $this->get_tool_launch_service(); // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items. $agsclaim = [ "scope" => [ "https://purl.imsglobal.org/spec/lti-ags/scope/score", "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", ], "lineitems" => "https://platform.example.com/10/lineitems" ]; // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, $agsclaim); $instructoruser = $this->getDataGenerator()->create_user(); [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); $student1user = $this->getDataGenerator()->create_user(); $student2user = $this->getDataGenerator()->create_user(); [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); // Grade student1 only. $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); // Mock task, asserting that line item creation takes place via a mock grade service object. $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); $mockgradeservice->method('putGrade')->willReturnCallback(function() { return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; }); $mockgradeservice->expects($this->once()) ->method('findOrCreateLineitem'); $mockgradeservice->expects($this->once()) ->method('putGrade') ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class)); $mocktask = $this->getMockBuilder(sync_tool_grades::class) ->onlyMethods(['get_ags']) ->getMock(); $mocktask->method('get_ags')->willReturn($mockgradeservice); // Sync and verify that only student1's grade is sent. ob_start(); $mocktask->set_custom_data($resource); $mocktask->execute(); $ob = ob_get_contents(); ob_end_clean(); $expectedtraces = [ "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". "'$course->id'.", "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". "'$resource->id' and the course '$course->id' was sent.", "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". "'$course->id'.", "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". "Processed 3 users; sent 1 grades." ]; foreach ($expectedtraces as $expectedtrace) { $this->assertStringContainsString($expectedtrace, $ob); } } } tests/local/ltiadvantage/task/sync_grades_test.php000064400000010644152266447110016473 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use enrol_lti\helper; use Packback\Lti1p3\LtiAssignmentsGradesService; use Packback\Lti1p3\LtiGrade; use Packback\Lti1p3\LtiLineitem; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the enrol_lti\local\ltiadvantage\task\sync_grades scheduled task. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_grades */ final class sync_grades_test extends \lti_advantage_testcase { /** * Test confirming task name. * * @covers ::get_name */ public function test_get_name(): void { $this->assertEquals(get_string('tasksyncgrades', 'enrol_lti'), (new sync_grades())->get_name()); } /** * Test grade sync when the resource has syncgrades disabled. * * @covers ::execute */ public function test_sync_grades_gradesync_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, false); $launchservice = $this->get_tool_launch_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); $task = new \enrol_lti\local\ltiadvantage\task\sync_grades(); $this->expectOutputRegex('/Skipping task - There are no resources with grade sync enabled./'); $task->execute(); } /** * Test the grade sync task when the auth_lti plugin is disabled. * * @covers ::execute */ public function test_sync_grades_auth_plugin_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(false); $launchservice = $this->get_tool_launch_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); $task = new \enrol_lti\local\ltiadvantage\task\sync_grades(); $this->expectOutputRegex('/Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')) . '/'); $task->execute(); } /** * Test the grade sync task when the enrol_lti plugin is disabled. * * @covers ::execute */ public function test_sync_grades_enrol_plugin_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, false); $launchservice = $this->get_tool_launch_service(); // Launch the resource for an instructor which will create the domain objects needed for service calls. $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); $instructoruser = $this->getDataGenerator()->create_user(); $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); $task = new \enrol_lti\local\ltiadvantage\task\sync_grades(); $this->expectOutputRegex('/Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti') . '/'); $task->execute(); } } tests/local/ltiadvantage/task/sync_members_test.php000064400000162644152266447110016670 0ustar00. namespace enrol_lti\local\ltiadvantage\task; use enrol_lti\helper; use enrol_lti\local\ltiadvantage\entity\user; use enrol_lti\local\ltiadvantage\repository\resource_link_repository; use enrol_lti\local\ltiadvantage\repository\user_repository; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../lti_advantage_testcase.php'); /** * Tests for the enrol_lti\local\ltiadvantage\task\sync_members scheduled task. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_members */ final class sync_members_test extends \lti_advantage_testcase { /** * Verify the user's profile picture has been set, which is useful to verify picture syncs. * * @param int $userid the id of the Moodle user. * @param bool $match true to verify a match, false to verify a non-match. */ protected function verify_user_profile_image(int $userid, bool $match = true): void { global $CFG; $user = \core_user::get_user($userid); $usercontext = \context_user::instance($user->id); $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture; $page = new \moodle_page(); $page->set_url('/user/profile.php'); $page->set_context(\context_system::instance()); $renderer = $page->get_renderer('core'); $userpicture = new \user_picture($user); if ($match) { $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); } else { $this->assertNotEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); } } /** * Helper to get a list of mocked member entries for use in the mocked sync task. * * @param array $userids the array of lti user ids to use. * @param array|null $legacyuserids legacy user ids for the lti11_legacy_user_id property, null if not desired. * @param bool $names whether to include names in the user data or not. * @param bool $emails whether to include email in the user data or not. * @param bool $linklevel whether to mock the user return data at link-level (true) or context-level (false). * @param bool $picture whether to mock a user's picture field in the return data. * @param array $roles an array of IMS roles to include with each member which, if empty, defaults to just the learner role. * @return array the array of users. * @throws \Exception if the legacyuserids array doesn't contain the correct number of ids. */ protected static function get_mock_members_with_ids( array $userids, ?array $legacyuserids = null, $names = true, $emails = true, bool $linklevel = true, bool $picture = false, array $roles = [], ): array { if (!is_null($legacyuserids) && count($legacyuserids) != count($userids)) { throw new \Exception('legacyuserids must contain the same number of ids as $userids.'); } if (empty($roles)) { $roles = ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner']; } $users = []; foreach ($userids as $userid) { $user = ['user_id' => (string) $userid, 'roles' => $roles]; if ($picture) { $user['picture'] = static::getExternalTestFileUrl('/test.jpg', false); } if ($names) { $user['given_name'] = 'Firstname' . $userid; $user['family_name'] = 'Surname' . $userid; } if ($emails) { $user['email'] = "firstname.surname{$userid}@lms.example.org"; } if ($legacyuserids) { $user['lti11_legacy_user_id'] = array_shift($legacyuserids); } if ($linklevel) { // Link-level memberships also include a message property. $user['message'] = [ 'https://purl.imsglobal.org/spec/lti/claim/message_type' => 'LtiResourceLinkRequest' ]; } $users[] = $user; } return $users; } /** * Gets a task mocked to only support resource-link-level memberships request. * * @param array $resourcelinks array for stipulating per link users, containing list of [resourcelink, members]. * @return sync_members|\PHPUnit\Framework\MockObject\MockObject */ protected function get_mock_task_resource_link_level(array $resourcelinks = []) { $mocktask = $this->getMockBuilder(sync_members::class) ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) ->getMock(); $mocktask->expects($this->any()) ->method('get_context_level_members') ->will($this->returnCallback(function() { return false; })); $expectedcount = !empty($resourcelinks) ? count($resourcelinks) : 1; $mocktask->expects($this->exactly($expectedcount)) ->method('get_resource_link_level_members') ->will($this->returnCallback(function ($nrpsinfo, $serviceconnector, $registration, $reslink) use ($resourcelinks) { if ($resourcelinks) { foreach ($resourcelinks as $rl) { if ($reslink->get_resourcelinkid() === $rl[0]->get_resourcelinkid()) { return $rl[1]; } } } else { return self::get_mock_members_with_ids(range(1, 2)); } })); return $mocktask; } /** * Gets a task mocked to only support context-level memberships request. * * @return sync_members|\PHPUnit\Framework\MockObject\MockObject */ protected function get_mock_task_context_level() { $mocktask = $this->getMockBuilder(sync_members::class) ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) ->getMock(); $mocktask->expects($this->any()) ->method('get_resource_link_level_members') ->will($this->returnCallback(function() { // An exception is what the service code will throw if the resource link level service isn't available. throw new \Exception(); })); $mocktask->expects($this->any()) ->method('get_context_level_members') ->will($this->returnCallback(function() { return self::get_mock_members_with_ids(range(1, 3), null, true, true, false); }));; return $mocktask; } /** * Gets a sync task, with the remote calls mocked to return the supplied users. * * See get_mock_members_with_ids() for generating the users for input. * * @param array $users a list of users, the result of a call to get_mock_members_with_ids(). * @return \PHPUnit\Framework\MockObject\MockObject the mock task. */ protected function get_mock_task_with_users(array $users) { $mocktask = $this->getMockBuilder(sync_members::class) ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members']) ->getMock(); $mocktask->expects($this->any()) ->method('get_context_level_members') ->will($this->returnCallback(function() { return false; })); $mocktask->expects($this->any()) ->method('get_resource_link_level_members') ->will($this->returnCallback(function () use ($users) { return $users; })); return $mocktask; } /** * Check that all the given ltiusers are enrolled in the course. * * @param \stdClass $course the course instance. * @param user[] $ltiusers array of lti user instances. */ protected function verify_course_enrolments(\stdClass $course, array $ltiusers) { global $CFG; require_once($CFG->libdir . '/enrollib.php'); $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); $this->assertCount(count($ltiusers), $enrolledusers); $enrolleduserids = array_map(function($stringid) { return (int) $stringid; }, array_column($enrolledusers, 'id')); foreach ($ltiusers as $ltiuser) { $this->assertContains($ltiuser->get_localid(), $enrolleduserids); } } /** * Test confirming task name. * * @covers ::get_name */ public function test_get_name(): void { $this->assertEquals(get_string('tasksyncmembers', 'enrol_lti'), (new sync_members())->get_name()); } /** * Test a resource-link-level membership sync, confirming that all relevant domain objects are updated properly. * * @covers ::execute */ public function test_resource_link_level_sync(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice = $this->get_tool_launch_service(); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_resource_link_level(); $task->execute(); // Verify 2 users and their corresponding course enrolments exist. $this->expectOutputRegex( "/Completed - Synced members for tool '$resource->id' in the course '$course->id'. ". "Processed 2 users; enrolled 2 members; unenrolled 0 members./" ); $userrepo = new user_repository(); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(2, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); } /** * Test a resource-link-level membership sync when there are more than one resource links for the resource. * * @covers ::execute */ public function test_resource_link_level_sync_multiple_resource_links(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); // Launch twice - once from each resource link in the platform. $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0], '123'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0], '456'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Now, grab the resource links. $rlrepo = new resource_link_repository(); $reslinks = $rlrepo->find_by_resource($resource->id); $mockmembers = self::get_mock_members_with_ids(range(1, 10)); $mockusers1 = array_slice($mockmembers, 0, 6); $mockusers2 = array_slice($mockmembers, 6); $resourcelinks = [ [$reslinks[0], $mockusers1], [$reslinks[1], $mockusers2] ]; // Sync the members, using the mock task set up to sync different sets of users for each resource link. $task = $this->get_mock_task_resource_link_level($resourcelinks); ob_start(); $task->execute(); $output = ob_get_contents(); ob_end_clean(); // Verify 10 users and their corresponding course enrolments exist. $userrepo = new user_repository(); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(10, $ltiusers); $this->assertStringContainsString("Completed - Synced 6 members for the resource link", $output); $this->assertStringContainsString("Completed - Synced 4 members for the resource link", $output); $this->assertStringContainsString("Completed - Synced members for tool '$resource->id' in the course '". "$resource->courseid'. Processed 10 users; enrolled 10 members; unenrolled 0 members.\n", $output); $this->verify_course_enrolments($course, $ltiusers); } /** * Verify the task will update users' profile pictures if the 'picture' member field is provided. * * @covers ::execute */ public function test_user_profile_image_sync(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['1'], null, true, true, true, true)); ob_start(); $task->execute(); ob_end_clean(); // Verify 1 users and their corresponding course enrolments exist. $userrepo = new user_repository(); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); // Verify user profile image has been updated. $this->verify_user_profile_image($ltiusers[0]->get_localid()); } /** * Test a context-level membership sync, confirming that all relevant domain objects are updated properly. * * @covers ::execute */ public function test_context_level_sync(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_context_level(); ob_start(); $task->execute(); ob_end_clean(); // Verify 3 users and their corresponding course enrolments exist. $userrepo = new user_repository(); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(3, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); } /** * Test verifying the sync task handles the omission/inclusion of PII information for users. * * @covers ::execute */ public function test_sync_user_data(): void { $this->resetAfterTest(); [$course, $resource, $resource2, $resource3, $appreg] = $this->create_test_environment(); $userrepo = new user_repository(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 5), null, false, false)); ob_start(); $task->execute(); ob_end_clean(); // Verify 5 users and their corresponding course enrolments exist. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(5, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); // Since user data wasn't included in the response, the users will have been synced using fallbacks, // so verify these. foreach ($ltiusers as $ltiuser) { $user = \core_user::get_user($ltiuser->get_localid()); // Firstname falls back to sourceid. $this->assertEquals($ltiuser->get_sourceid(), $user->firstname); // Lastname falls back to resource context id. $this->assertEquals($appreg->get_platformid(), $user->lastname); // Email falls back to example.com. $issuersubhash = sha1($appreg->get_platformid() . '_' . $ltiuser->get_sourceid()); $this->assertEquals("enrol_lti_13_{$issuersubhash}@example.com", $user->email); } // Sync again, this time with user data included. $mockmembers = self::get_mock_members_with_ids(range(1, 5)); $task = $this->get_mock_task_with_users($mockmembers); ob_start(); $task->execute(); ob_end_clean(); // User data was included in the response and should have been updated. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(5, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); foreach ($ltiusers as $ltiuser) { $user = \core_user::get_user($ltiuser->get_localid()); $mockmemberindex = array_search($ltiuser->get_sourceid(), array_column($mockmembers, 'user_id')); $mockmember = $mockmembers[$mockmemberindex]; $this->assertEquals($mockmember['given_name'], $user->firstname); $this->assertEquals($mockmember['family_name'], $user->lastname); $this->assertEquals($mockmember['email'], $user->email); } } /** * Test verifying the task won't sync members for shared resources having member sync disabled. * * @covers ::execute */ public function test_membership_sync_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, true, false); // Launch the tool for a user. $mockuser = self::get_mock_launch_users_with_ids(['1'])[0]; $mocklaunch = $this->get_mock_launch($resource, $mockuser); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_with_users(self::get_mock_launch_users_with_ids(range(1, 4))); ob_start(); $task->execute(); ob_end_clean(); // Verify no users were added or removed. // A single user (the user who launched the resource link) is expected. $userrepo = new user_repository(); $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(1, $ltiusers); $this->assertEquals($mockuser['user_id'], $ltiusers[0]->get_sourceid()); $this->verify_course_enrolments($course, $ltiusers); } /** * Test verifying the sync task for resources configured as 'helper::MEMBER_SYNC_ENROL_AND_UNENROL'. * * @covers ::execute */ public function test_sync_mode_enrol_and_unenrol(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $userrepo = new user_repository(); // Launch the tool for a user. $mockuser = self::get_mock_launch_users_with_ids(['1'])[0]; $mocklaunch = $this->get_mock_launch($resource, $mockuser); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); // Sync members. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 3))); ob_start(); $task->execute(); ob_end_clean(); // Verify 3 users and their corresponding course enrolments exist. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(3, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); // Now, simulate a subsequent sync in which 1 existing user maintains access, // 2 existing users are unenrolled and 3 new users are enrolled. $task2 = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['1', '4', '5', '6'])); ob_start(); $task2->execute(); ob_end_clean(); // Verify the missing users have been unenrolled and new users enrolled. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(4, $ltiusers); $unenrolleduserids = ['2', '3']; $enrolleduserids = ['1', '4', '5', '6']; foreach ($ltiusers as $ltiuser) { $this->assertNotContains($ltiuser->get_sourceid(), $unenrolleduserids); $this->assertContains($ltiuser->get_sourceid(), $enrolleduserids); } $this->verify_course_enrolments($course, $ltiusers); } /** * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_UNENROL_MISSING'. * * @covers ::execute */ public function test_sync_mode_unenrol_missing(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_UNENROL_MISSING); $userrepo = new user_repository(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); // Sync members using a payload which doesn't include the original launch user (User id = 1). $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 3))); ob_start(); $task->execute(); ob_end_clean(); // Verify the original user (launching user) has been unenrolled and that no new members have been enrolled. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(0, $ltiusers); } /** * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_ENROL_NEW'. * * @covers ::execute */ public function test_sync_mode_enrol_new(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_NEW); $userrepo = new user_repository(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); // Sync members using a payload which includes two new members only (i.e. not the original launching user). $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 3))); ob_start(); $task->execute(); ob_end_clean(); // Verify we now have 3 enrolments. The original user (who was not unenrolled) and the 2 new users. $ltiusers = $userrepo->find_by_resource($resource->id); $this->assertCount(3, $ltiusers); $this->verify_course_enrolments($course, $ltiusers); } /** * Test confirming that no changes take place if the auth_lti plugin is not enabled. * * @covers ::execute */ public function test_sync_auth_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(false); $userrepo = new user_repository(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); // If the task were to run, this would trigger 1 unenrolment (the launching user) and 3 enrolments. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2))); $task->execute(); // Verify that the sync didn't take place. $this->expectOutputRegex("/Skipping task - Authentication plugin 'LTI' is not enabled/"); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); } /** * Test confirming that no sync takes place when the enrol_lti plugin is not enabled. * * @covers ::execute */ public function test_sync_enrol_disabled(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(true, false); $userrepo = new user_repository(); // Launch the tool for a user. $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); // If the task were to run, this would trigger 1 unenrolment of the launching user and enrolment of 3 users. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2))); $task->execute(); // Verify that the sync didn't take place. $this->expectOutputRegex("/Skipping task - The 'Publish as LTI tool' plugin is disabled/"); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); } /** * Test syncing members when the enrolment instance is disabled. * * @covers ::execute */ public function test_sync_members_disabled_instance(): void { $this->resetAfterTest(); global $DB; [$course, $resource, $resource2, $resource3] = $this->create_test_environment(); $userrepo = new user_repository(); // Disable resource 1. $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED]; $DB->update_record('enrol', $enrol); // Delete the activity being shared by resource2, leaving resource 2 disabled as a result. $modcontext = \context::instance_by_id($resource2->contextid); course_delete_module($modcontext->instanceid); // Only the enabled resource 3 should sync members. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 1))); $task->execute(); $this->expectOutputRegex( "/^Starting - Member sync for published resource '$resource3->id' for course '$course->id'.\n". "Completed - Synced members for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ". "enrolled 0 members; unenrolled 0 members.\n$/" ); $this->assertCount(0, $userrepo->find_by_resource($resource->id)); } /** * Test syncing members for a membersync-enabled resource when the launch omits the NRPS service endpoints. * * @covers ::execute */ public function test_sync_no_nrps_support(): void { $this->resetAfterTest(); [$course, $resource] = $this->create_test_environment(); $userrepo = new user_repository(); // Launch the tool for a user. $mockinstructor = self::get_mock_launch_users_with_ids([1])[0]; $mocklaunch = $this->get_mock_launch($resource, $mockinstructor, null, null, false); $launchservice = $this->get_tool_launch_service(); $instructoruser = $this->lti_advantage_user_authenticates('1'); $launchservice->user_launches_tool($instructoruser, $mocklaunch); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); // The task would sync an additional 2 users if the link had NRPS service support. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2))); // We expect the task to report that it is skipping the resource due to a lack of NRPS support. $task->execute(); // Verify no enrolments or unenrolments. $this->expectOutputRegex( "/Skipping - No names and roles service found.\n". "Completed - Synced members for tool '{$resource->id}' in the course '{$course->id}'. ". "Processed 0 users; enrolled 0 members; unenrolled 0 members./" ); $this->assertCount(1, $userrepo->find_by_resource($resource->id)); } /** * Test confirming that preexisting, non-lti user accounts do not have their profiles or pictures updated during sync. * * @covers ::execute */ public function test_sync_non_lti_linked_user(): void { $this->resetAfterTest(); // Set up the environment. [$course, $resource] = $this->create_test_environment(); // Fake an auth - making sure it's a manual account. $authenticateduser = $this->lti_advantage_user_authenticates('123'); $authenticateduser->auth = 'manual'; $authenticateduser->password = '1234abcD*'; user_update_user($authenticateduser); $authenticateduser = \core_user::get_user($authenticateduser->id); // Mock the launch for the specified user. $mocklaunchuser = self::get_mock_launch_users_with_ids([$authenticateduser->id])[0]; $mocklaunch = $this->get_mock_launch($resource, $mocklaunchuser); $this->get_tool_launch_service()->user_launches_tool($authenticateduser, $mocklaunch); // Prepare the sync task, with a stubbed list of members. $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['123'], null, true, true, true, true)); // Run the member sync. $this->expectOutputRegex( "/Skipped profile sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method.\n" . "Skipped picture sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method/" ); $task->execute(); $updateduser = \core_user::get_user($authenticateduser->id); $this->assertEquals($authenticateduser->firstname, $updateduser->firstname); $this->assertEquals($authenticateduser->lastname, $updateduser->lastname); $this->assertEquals($authenticateduser->email, $updateduser->email); $this->verify_user_profile_image($authenticateduser->id, false); } /** * Test the member sync for a range of scenarios including migrated tools, unlaunched tools, provisioning methods. * * @dataProvider member_sync_data_provider * @param array|null $legacydata array detailing what legacy information to create, or null if not required. * @param array|null $resourceconfig array detailing config values to be used when creating the test enrol_lti instances. * @param array $launchdata array containing details of the launch, including user and migration claim. * @param array|null $syncmembers the members to use in the mock sync. * @param array $expected the array detailing expectations. * @covers ::execute */ public function test_sync_enrolments_and_migration(?array $legacydata, ?array $resourceconfig, array $launchdata, ?array $syncmembers, array $expected): void { $this->resetAfterTest(); // Set up the environment. [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, false, 0, $resourceconfig['provisioningmodeinstructor'] ?? 0, $resourceconfig['provisioningmodelearner'] ?? 0); // Set up legacy tool and user data. if ($legacydata) { [$legacytools, $legacyconsumerrecord, $legacyusers] = $this->setup_legacy_data($course, $legacydata); } // Mock the launch for the specified user. $mocklaunch = $this->get_mock_launch($resource, $launchdata['user'], null, [], true, $launchdata['launch_migration_claim']); // Perform the launch. $instructoruser = $this->lti_advantage_user_authenticates( $launchdata['user']['user_id'], $launchdata['launch_migration_claim'] ?? [] ); $this->get_tool_launch_service()->user_launches_tool($instructoruser, $mocklaunch); // Prepare the sync task, with a stubbed list of members. $task = $this->get_mock_task_with_users($syncmembers); // Run the member sync. ob_start(); $task->execute(); ob_end_clean(); // Verify enrolments. $ltiusers = (new user_repository())->find_by_resource($resource->id); $enrolled = array_filter($expected['enrolments'], function($user) { return $user['is_enrolled']; }); $this->assertCount(count($enrolled), $ltiusers); $this->verify_course_enrolments($course, $ltiusers); // Verify migration, if expected. if ($legacydata) { $legacyuserids = array_column($legacyusers, 'id'); foreach ($ltiusers as $ltiuser) { $this->assertArrayHasKey($ltiuser->get_sourceid(), $expected['enrolments']); if (!$expected['enrolments'][$ltiuser->get_sourceid()]['is_migrated']) { // Those members who hadn't launched over 1p1 prior will have new lti user records created. $this->assertNotContains((string)$ltiuser->get_localid(), $legacyuserids); } else { // Those members who were either already migrated during launch, or were migrated during the sync, // will be mapped to their legacy user accounts. $this->assertContains((string)$ltiuser->get_localid(), $legacyuserids); } } } } /** * Data provider for member syncs. * * @return array[] the array of test data. */ public static function member_sync_data_provider(): array { global $CFG; require_once($CFG->dirroot . '/auth/lti/auth.php'); return [ 'Migrated tool, user ids changed, new and existing users present in sync' => [ 'legacydata' => [ 'users' => [ ['user_id' => '1'], ['user_id' => '2'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '1', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'syncmembers' => [ self::get_mock_members_with_ids(['1p3_1'], ['1'])[0], self::get_mock_members_with_ids(['1p3_2'], ['2'])[0], self::get_mock_members_with_ids(['1p3_3'], ['3'])[0], self::get_mock_members_with_ids(['1p3_4'], ['4'])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, 'is_migrated' => true, ], '1p3_2' => [ 'is_enrolled' => true, 'is_migrated' => true, ], '1p3_3' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => true, 'is_migrated' => false, ] ] ] ], 'Migrated tool, no change in user ids, new and existing users present in sync' => [ 'legacydata' => [ 'users' => [ ['user_id' => '1'], ['user_id' => '2'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'syncmembers' => [ self::get_mock_members_with_ids(['1'], null)[0], self::get_mock_members_with_ids(['2'], null)[0], self::get_mock_members_with_ids(['3'], null)[0], self::get_mock_members_with_ids(['4'], null)[0], ], 'expected' => [ 'enrolments' => [ '1' => [ 'is_enrolled' => true, 'is_migrated' => true, ], '2' => [ 'is_enrolled' => true, 'is_migrated' => true, ], '3' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '4' => [ 'is_enrolled' => true, 'is_migrated' => false, ] ] ] ], 'New tool, no launch migration claim, change in user ids, new and existing users present in sync' => [ 'legacydata' => [ 'users' => [ ['user_id' => '1'], ['user_id' => '2'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ self::get_mock_members_with_ids(['1p3_1'], null)[0], self::get_mock_members_with_ids(['1p3_2'], null)[0], self::get_mock_members_with_ids(['1p3_3'], null)[0], self::get_mock_members_with_ids(['1p3_4'], null)[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_3' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => true, 'is_migrated' => false, ] ] ] ], 'New tool, no launch migration claim, no change in user ids, new and existing users present in sync' => [ 'legacydata' => [ 'users' => [ ['user_id' => '1'], ['user_id' => '2'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ self::get_mock_members_with_ids(['1'], null)[0], self::get_mock_members_with_ids(['2'], null)[0], self::get_mock_members_with_ids(['3'], null)[0], self::get_mock_members_with_ids(['4'], null)[0], ], 'expected' => [ 'enrolments' => [ '1' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '2' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '3' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '4' => [ 'is_enrolled' => true, 'is_migrated' => false, ] ] ] ], 'New tool, migration only via member sync, no launch claim, new and existing users present in sync' => [ 'legacydata' => [ 'users' => [ ['user_id' => '1'], ['user_id' => '2'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => null, 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ self::get_mock_members_with_ids(['1p3_1'], ['1'])[0], self::get_mock_members_with_ids(['1p3_2'], ['2'])[0], self::get_mock_members_with_ids(['1p3_3'], ['3'])[0], self::get_mock_members_with_ids(['1p3_4'], ['4'])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_3' => [ 'is_enrolled' => true, 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => true, 'is_migrated' => false, ] ] ] ], 'Default provisioning modes, mixed bag of users and roles' => [ 'legacydata' => null, 'resourceconfig' => [ 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ // This user is just an instructor but is also the user who is already linked, via the launch above. self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ])[0], // This user is just a learner. self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is also a learner. self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is both an instructor and a learner. self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => false, ], '1p3_3' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => false, // Both roles - not enrolled due to instructor's 'prompt' provisioning mode. 'is_migrated' => false, ] ] ] ], 'All automatic provisioning, mixed bag of users and roles' => [ 'legacydata' => null, 'resourceconfig' => [ 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ // This user is just an instructor but is also the user who is already linked, via the launch above. self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ])[0], // This user is just a learner. self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is also a learner. self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is both an instructor and a learner. self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => false, ], '1p3_3' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode. 'is_migrated' => false, ] ] ] ], 'All prompt provisioning, mixed bag of users and roles' => [ 'legacydata' => null, 'resourceconfig' => [ 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => null, ], 'syncmembers' => [ // This user is just an instructor but is also the user who is already linked, via the launch above. self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ])[0], // This user is just a learner. self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is also a learner. self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is both an instructor and a learner. self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode. 'is_migrated' => false, ], '1p3_3' => [ 'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode. 'is_migrated' => false, ], '1p3_4' => [ 'is_enrolled' => false, // Both roles - not enrolled due to instructor's 'prompt' provisioning mode. 'is_migrated' => false, ] ] ] ], 'All automatic provisioning, with legacy data and migration claim, mixed bag of users and roles' => [ 'legacydata' => [ 'users' => [ ['user_id' => '2'], ['user_id' => '3'], ['user_id' => '4'], ['user_id' => '5'] ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'resourceconfig' => [ 'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY, 'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY ], 'launchdata' => [ 'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0], 'launch_migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ], ], 'syncmembers' => [ // This user is just an instructor but is also the user who is already linked, via the launch above. self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ])[0], // This user is just a learner. self::get_mock_members_with_ids(['1p3_2'], ['2'], true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is also a learner. self::get_mock_members_with_ids(['1p3_3'], ['3'], true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is both an instructor and a learner. self::get_mock_members_with_ids(['1p3_4'], ['4'], true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ])[0], // This user is just an instructor who hasn't launched before (unlike the first user here). self::get_mock_members_with_ids(['1p3_5'], ['5'], true, true, true, false, [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', ])[0], ], 'expected' => [ 'enrolments' => [ '1p3_1' => [ 'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked). 'is_migrated' => false, ], '1p3_2' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => true, ], '1p3_3' => [ 'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode. 'is_migrated' => true, ], '1p3_4' => [ 'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode. 'is_migrated' => true ], '1p3_5' => [ 'is_enrolled' => true, // Instructor role only - enrolled due to instructor's 'auto' provisioning mode. 'is_migrated' => true ] ] ] ], ]; } } tests/local/ltiadvantage/utility/message_helper_test.php000064400000010020152266447110017702 0ustar00. namespace enrol_lti\local\ltiadvantage\utility; /** * Test cases for the enrol_lti\local\ltiadvantage\utility\message_helper utility class. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \enrol_lti\local\ltiadvantage\utility\message_helper */ final class message_helper_test extends \base_testcase { /** * Test the static helper is_instructor_launch. * * @dataProvider message_roles_provider * @param array $jwtdata the mock JWT data from a launch. * @param bool $expected the expected return of is_instructor_launch() given the JWT data. * @covers ::is_instructor_launch */ public function test_is_instructor_launch(array $jwtdata, bool $expected): void { $this->assertEquals($expected, message_helper::is_instructor_launch($jwtdata)); } /** * Data provider for testing role helpers. * * @return array the array of test JWT data. */ public static function message_roles_provider(): array { return [ 'Roles claim present, includes learner role only' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ] ], 'expected' => false ], 'Roles claim present, includes teacher role only' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' ] ], 'expected' => true ], 'Roles claim present, includes admin role only' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator' ] ], 'expected' => true ], 'Roles claim present, includes learner and teacher roles' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ] ], 'expected' => true ], 'Roles claim present, includes instructor role using legacy short name and learner role' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [ 'Instructor', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ] ], 'expected' => true ], 'Roles claim empty' => [ 'jwtdata' => [ 'https://purl.imsglobal.org/spec/lti/claim/roles' => [], ], 'expected' => false ], 'Roles claim not present' => [ 'jwtdata' => [], 'expected' => false ], ]; } } tests/data_connector_test.php000064400000140407152266447110012457 0ustar00. namespace enrol_lti; use enrol_lti\data_connector; use IMSGlobal\LTI\ToolProvider\ConsumerNonce; use IMSGlobal\LTI\ToolProvider\Context; use IMSGlobal\LTI\ToolProvider\ResourceLink; use IMSGlobal\LTI\ToolProvider\ResourceLinkShare; use IMSGlobal\LTI\ToolProvider\ResourceLinkShareKey; use IMSGlobal\LTI\ToolProvider\ToolConsumer; use IMSGlobal\LTI\ToolProvider\ToolProvider; use IMSGlobal\LTI\ToolProvider\ToolProxy; use IMSGlobal\LTI\ToolProvider\User; /** * Test the data_connector class. * * @package enrol_lti * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class data_connector_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Set this user as the admin. $this->setAdminUser(); } /** * Test for data_connector::loadToolConsumer(). */ public function test_load_consumer(): void { $consumer = new ToolConsumer(); $dc = new data_connector(); // Consumer has not been saved to the database, so this should return false. $this->assertFalse($dc->loadToolConsumer($consumer)); // Save a consumer into the DB. $time = time(); $data = [ 'name' => 'TestName', 'secret' => 'TestSecret', 'ltiversion' => ToolProvider::LTI_VERSION1, 'consumername' => 'TestConsumerName', 'consumerversion' => 'TestConsumerVersion', 'consumerguid' => 'TestConsumerGuid', 'profile' => json_decode('{TestProfile}'), 'toolproxy' => 'TestProxy', 'settings' => ['setting1' => 'TestSetting 1', 'setting2' => 'TestSetting 2'], 'protected' => 1, 'enabled' => 0, 'enablefrom' => $time, 'enableuntil' => $time + 1, 'lastaccess' => strtotime(date('Y-m-d')), ]; $consumer->name = $data['name']; $consumer->setKey('TestKey'); $consumer->secret = $data['secret']; $consumer->ltiVersion = $data['ltiversion']; $consumer->consumerName = $data['consumername']; $consumer->consumerVersion = $data['consumerversion']; $consumer->consumerGuid = $data['consumerguid']; $consumer->profile = $data['profile']; $consumer->toolProxy = $data['toolproxy']; $consumer->setSettings($data['settings']); $consumer->protected = true; $consumer->enabled = false; $consumer->enableFrom = $data['enablefrom']; $consumer->enableUntil = $data['enableuntil']; $consumer->lastAccess = $data['lastaccess']; $dc->saveToolConsumer($consumer); $this->assertTrue($dc->loadToolConsumer($consumer)); $this->assertEquals($consumer->name, 'TestName'); $this->assertEquals($consumer->getKey(), 'TestKey'); $this->assertEquals($consumer->secret, 'TestSecret'); $this->assertEquals($consumer->ltiVersion, $data['ltiversion']); $this->assertEquals($consumer->consumerName, $data['consumername']); $this->assertEquals($consumer->consumerVersion, $data['consumerversion']); $this->assertEquals($consumer->consumerGuid, $data['consumerguid']); $this->assertEquals($consumer->profile, $data['profile']); $this->assertEquals($consumer->toolProxy, $data['toolproxy']); $this->assertEquals($consumer->getSettings(), $data['settings']); $this->assertTrue($consumer->protected); $this->assertFalse($consumer->enabled); $this->assertEquals($consumer->enableFrom, $data['enablefrom']); $this->assertEquals($consumer->enableUntil, $data['enableuntil']); $this->assertEquals($consumer->lastAccess, $data['lastaccess']); } /** * Test for data_connector::saveToolConsumer(). */ public function test_save_consumer(): void { $dc = new data_connector(); $time = time(); $data = [ 'name' => 'TestName', 'secret' => 'TestSecret', 'ltiversion' => ToolProvider::LTI_VERSION1, 'consumername' => 'TestConsumerName', 'consumerversion' => 'TestConsumerVersion', 'consumerguid' => 'TestConsumerGuid', 'profile' => json_decode('{TestProfile}'), 'toolproxy' => 'TestProxy', 'settings' => ['setting1' => 'TestSetting 1', 'setting2' => 'TestSetting 2'], 'protected' => 1, 'enabled' => 0, 'enablefrom' => $time, 'enableuntil' => $time + 1, 'lastaccess' => strtotime(date('Y-m-d')), ]; $consumer = new ToolConsumer(); $consumer->name = $data['name']; $consumer->setKey('TestKey'); $consumer->secret = $data['secret']; $consumer->ltiVersion = $data['ltiversion']; $consumer->consumerName = $data['consumername']; $consumer->consumerVersion = $data['consumerversion']; $consumer->consumerGuid = $data['consumerguid']; $consumer->profile = $data['profile']; $consumer->toolProxy = $data['toolproxy']; $consumer->setSettings($data['settings']); $consumer->protected = true; $consumer->enabled = false; $consumer->enableFrom = $data['enablefrom']; $consumer->enableUntil = $data['enableuntil']; $consumer->lastAccess = $data['lastaccess']; // Save new consumer into the DB. $this->assertTrue($dc->saveToolConsumer($consumer)); // Check saved values. $this->assertEquals($consumer->name, $data['name']); $this->assertEquals($consumer->getKey(), 'TestKey'); $this->assertEquals($consumer->secret, $data['secret']); $this->assertEquals($consumer->ltiVersion, $data['ltiversion']); $this->assertEquals($consumer->consumerName, $data['consumername']); $this->assertEquals($consumer->consumerVersion, $data['consumerversion']); $this->assertEquals($consumer->consumerGuid, $data['consumerguid']); $this->assertEquals($consumer->profile, $data['profile']); $this->assertEquals($consumer->toolProxy, $data['toolproxy']); $this->assertEquals($consumer->getSettings(), $data['settings']); $this->assertTrue($consumer->protected); $this->assertFalse($consumer->enabled); $this->assertEquals($consumer->enableFrom, $data['enablefrom']); $this->assertEquals($consumer->enableUntil, $data['enableuntil']); $this->assertEquals($consumer->lastAccess, $data['lastaccess']); // Edit values. $edit = 'EDIT'; $consumer->name = $data['name'] . $edit; $consumer->setKey('TestKey' . $edit); $consumer->secret = $data['secret'] . $edit; $consumer->ltiVersion = ToolProvider::LTI_VERSION2; $consumer->consumerName = $data['consumername'] . $edit; $consumer->consumerVersion = $data['consumerversion'] . $edit; $consumer->consumerGuid = $data['consumerguid'] . $edit; $editprofile = json_decode('{TestProfile}'); $consumer->profile = $editprofile; $consumer->toolProxy = $data['toolproxy'] . $edit; $editsettings = ['setting1' => 'TestSetting 1' . $edit, 'setting2' => 'TestSetting 2' . $edit]; $consumer->setSettings($editsettings); $consumer->protected = null; $consumer->enabled = null; $consumer->enableFrom = $data['enablefrom'] + 100; $consumer->enableUntil = $data['enableuntil'] + 100; // Save edited values. $this->assertTrue($dc->saveToolConsumer($consumer)); // Check edited values. $this->assertEquals($consumer->name, $data['name'] . $edit); $this->assertEquals($consumer->getKey(), 'TestKey' . $edit); $this->assertEquals($consumer->secret, $data['secret'] . $edit); $this->assertEquals($consumer->ltiVersion, ToolProvider::LTI_VERSION2); $this->assertEquals($consumer->consumerName, $data['consumername'] . $edit); $this->assertEquals($consumer->consumerVersion, $data['consumerversion'] . $edit); $this->assertEquals($consumer->consumerGuid, $data['consumerguid'] . $edit); $this->assertEquals($consumer->profile, $editprofile); $this->assertEquals($consumer->toolProxy, $data['toolproxy'] . $edit); $this->assertEquals($consumer->getSettings(), $editsettings); $this->assertNull($consumer->protected); $this->assertNull($consumer->enabled); $this->assertEquals($consumer->enableFrom, $data['enablefrom'] + 100); $this->assertEquals($consumer->enableUntil, $data['enableuntil'] + 100); } /** * Test for data_connector::deleteToolConsumer(). */ public function test_delete_tool_consumer(): void { $dc = new data_connector(); $data = [ 'name' => 'TestName', 'secret' => 'TestSecret', 'ltiversion' => ToolProvider::LTI_VERSION1, ]; $consumer = new ToolConsumer(null, $dc); $consumer->name = $data['name']; $consumer->setKey('TestKey'); $consumer->secret = $data['secret']; $consumer->save(); $nonce = new ConsumerNonce($consumer, 'testnonce'); $nonce->save(); $context = Context::fromConsumer($consumer, 'testlticontext'); $context->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->setContextId($context->getRecordId()); $resourcelink->save(); $this->assertEquals($consumer->getRecordId(), $resourcelink->getConsumer()->getRecordId()); $resourcelinkchild = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid'); $resourcelinkchild->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild->shareApproved = true; $resourcelinkchild->setContextId($context->getRecordId()); $resourcelinkchild->save(); $this->assertEquals($consumer->getRecordId(), $resourcelinkchild->getConsumer()->getRecordId()); $this->assertEquals($resourcelink->getRecordId(), $resourcelinkchild->primaryResourceLinkId); $this->assertTrue($resourcelinkchild->shareApproved); $resourcelinkchild2 = clone $resourcelink; $resourcelinkchild2->setRecordId(null); $resourcelinkchild2->setConsumerId(null); $resourcelinkchild2->setContextId(0); $resourcelinkchild2->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild2->shareApproved = true; $resourcelinkchild2->save(); $this->assertNull($resourcelinkchild2->getConsumer()->getRecordId()); $this->assertEquals(0, $resourcelinkchild2->getContextId()); $this->assertNotEquals($resourcelink->getRecordId(), $resourcelinkchild2->getRecordId()); $resourcelinksharekey = new ResourceLinkShareKey($resourcelink); $resourcelinksharekey->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; $dc->saveUser($user); // Confirm that tool consumer deletion processing ends successfully. $this->assertTrue($dc->deleteToolConsumer($consumer)); // Consumer object should have been initialised. foreach ($consumer as $key => $value) { $this->assertTrue(empty($value)); } // Nonce record should have been deleted. $this->assertFalse($dc->loadConsumerNonce($nonce)); // Share key record record should have been deleted. $this->assertFalse($dc->loadResourceLinkShareKey($resourcelinksharekey)); // Resource record link should have been deleted. $this->assertFalse($dc->loadResourceLink($resourcelink)); // Consumer record should have been deleted. $this->assertFalse($dc->loadToolConsumer($consumer)); // Resource links for contexts in this consumer should have been deleted. Even child ones. $this->assertFalse($dc->loadResourceLink($resourcelinkchild)); // Child resource link primaryResourceLinkId and shareApproved attributes should have been set to null. $this->assertTrue($dc->loadResourceLink($resourcelinkchild2)); $this->assertNull($resourcelinkchild2->primaryResourceLinkId); $this->assertNull($resourcelinkchild2->shareApproved); } /** * Test for data_connector::getToolConsumers(). */ public function test_get_tool_consumers(): void { $dc = new data_connector(); $consumers = $dc->getToolConsumers(); // Does not return null. $this->assertNotNull($consumers); // But returns empty array when no consumers found. $this->assertEmpty($consumers); $data = [ 'name' => 'TestName', 'secret' => 'TestSecret', 'ltiversion' => ToolProvider::LTI_VERSION1, ]; $count = 3; for ($i = 0; $i < $count; $i++) { $consumer = new ToolConsumer(null, $dc); $consumer->name = $data['name'] . $i; $consumer->setKey('TestKey' . $i); $consumer->secret = $data['secret'] . $i; $consumer->ltiVersion = $data['ltiversion']; $consumer->save(); } $consumers = $dc->getToolConsumers(); $this->assertNotEmpty($consumers); $this->assertCount($count, $consumers); // Check values. foreach ($consumers as $index => $record) { $this->assertEquals($data['name'] . $index, $record->name); $this->assertEquals('TestKey' . $index, $record->getKey()); $this->assertEquals($data['secret'] . $index, $record->secret); $record->ltiVersion = $data['ltiversion']; } } /** * Test for data_connector::loadToolProxy(). */ public function test_get_tool_proxy(): void { $dc = new data_connector(); $toolproxy = new ToolProxy($dc); $this->assertFalse($dc->loadToolProxy($toolproxy)); } /** * Test for data_connector::saveToolProxy(). */ public function test_save_tool_proxy(): void { $dc = new data_connector(); $toolproxy = new ToolProxy($dc); $this->assertFalse($dc->saveToolProxy($toolproxy)); } /** * Test for data_connector::deleteToolProxy(). */ public function test_delete_tool_proxy(): void { $dc = new data_connector(); $toolproxy = new ToolProxy($dc); $this->assertFalse($dc->deleteToolProxy($toolproxy)); } /** * Test for data_connector::loadContext(). */ public function test_load_context(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Load an unsaved context. $this->assertFalse($dc->loadContext($context)); // Save the context. $dc->saveContext($context); $created = $context->created; $updated = $context->updated; // Load saved context. $this->assertTrue($dc->loadContext($context)); $this->assertEquals($consumer, $context->getConsumer()); $this->assertEquals($title, $context->title); $this->assertEquals($settings, $context->getSettings()); $this->assertEquals($lticontextid, $context->ltiContextId); $this->assertEquals($created, $context->created); $this->assertEquals($updated, $context->updated); } /** * Test for data_connector::saveContext(). */ public function test_save_context(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Save the context. $this->assertTrue($dc->saveContext($context)); $id = $context->getRecordId(); $created = $context->created; $updated = $context->updated; // Check saved values. $this->assertNotNull($id); $this->assertNotEmpty($created); $this->assertNotEmpty($updated); $this->assertEquals($consumer, $context->getConsumer()); $this->assertEquals($title, $context->title); $this->assertEquals($settings, $context->getSettings()); $this->assertEquals($lticontextid, $context->ltiContextId); // Edit the context details. $newsettings = array_merge($settings, ['d', 'e']); $context->title = $title . 'edited'; $context->settings = $newsettings; $context->ltiContextId = $lticontextid . 'edited'; // Confirm that edited context is saved successfully. $this->assertTrue($dc->saveContext($context)); // Check edited values. $this->assertEquals($title . 'edited', $context->title); $this->assertEquals($newsettings, $context->getSettings()); $this->assertEquals($lticontextid . 'edited', $context->ltiContextId); // Created time stamp should not change. $this->assertEquals($created, $context->created); // Updated time stamp should have been changed. $this->assertGreaterThanOrEqual($updated, $context->updated); } /** * Test for data_connector::deleteContext(). */ public function test_delete_context(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Save the context. $this->assertTrue($dc->saveContext($context)); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->setContextId($context->getRecordId()); $resourcelink->save(); $this->assertEquals($consumer->getRecordId(), $resourcelink->getConsumer()->getRecordId()); $resourcelinkchild = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid'); $resourcelinkchild->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild->shareApproved = true; $resourcelinkchild->setContextId($context->getRecordId()); $resourcelinkchild->save(); $this->assertEquals($consumer->getRecordId(), $resourcelinkchild->getConsumer()->getRecordId()); $this->assertEquals($resourcelink->getRecordId(), $resourcelinkchild->primaryResourceLinkId); $this->assertTrue($resourcelinkchild->shareApproved); $resourcelinkchild2 = clone $resourcelink; $resourcelinkchild2->setRecordId(null); $resourcelinkchild2->setConsumerId(null); $resourcelinkchild2->setContextId(0); $resourcelinkchild2->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild2->shareApproved = true; $resourcelinkchild2->save(); $this->assertNull($resourcelinkchild2->getConsumer()->getRecordId()); $this->assertEquals(0, $resourcelinkchild2->getContextId()); $this->assertNotEquals($resourcelink->getRecordId(), $resourcelinkchild2->getRecordId()); $resourcelinksharekey = new ResourceLinkShareKey($resourcelink); $resourcelinksharekey->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; $dc->saveUser($user); $this->assertTrue($dc->deleteContext($context)); // Context properties should have been reset. $this->assertEmpty($context->title); $this->assertEmpty($context->settings); $this->assertNull($context->created); $this->assertNull($context->updated); // Context record should have already been deleted from the DB. $this->assertFalse($dc->loadContext($context)); // Share key record record should have been deleted. $this->assertFalse($dc->loadResourceLinkShareKey($resourcelinksharekey)); // Resource record link should have been deleted. $this->assertFalse($dc->loadResourceLink($resourcelink)); // Resource links for contexts in this consumer should have been deleted. Even child ones. $this->assertFalse($dc->loadResourceLink($resourcelinkchild)); // Child resource link primaryResourceLinkId and shareApproved attributes should have been set to null. $this->assertTrue($dc->loadResourceLink($resourcelinkchild2)); $this->assertNull($resourcelinkchild2->primaryResourceLinkId); $this->assertNull($resourcelinkchild2->shareApproved); } /** * Test for data_connector::loadResourceLink(). */ public function test_load_resource_link(): void { $dc = new data_connector(); // Consumer for the resource link. $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); // Context for the resource link. $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Save the context. $context->save(); $ltiresourcelinkid = 'testltiresourcelinkid'; $resourcelink = ResourceLink::fromConsumer($consumer, $ltiresourcelinkid); $resourcelink->setContextId($context->getRecordId()); $resourcelink->setSettings($settings); $resourcelink->shareApproved = true; $resourcelink->primaryResourceLinkId = 999; // Try to load an unsaved resource link. $this->assertFalse($dc->loadResourceLink($resourcelink)); // Save the resource link. $resourcelink->save(); // Load saved resource link. $this->assertTrue($dc->loadResourceLink($resourcelink)); $this->assertNotEmpty($resourcelink->getRecordId()); $this->assertEquals($settings, $resourcelink->getSettings()); $this->assertTrue($resourcelink->shareApproved); $this->assertEquals(999, $resourcelink->primaryResourceLinkId); $this->assertNotEmpty($resourcelink->created); $this->assertNotEmpty($resourcelink->updated); // Create another resource link instance similar to the first one. $resourcelink2 = ResourceLink::fromConsumer($consumer, $ltiresourcelinkid); $resourcelink2->setContextId($context->getRecordId()); // This should load the previous resource link. $this->assertTrue($dc->loadResourceLink($resourcelink2)); $this->assertEquals($resourcelink, $resourcelink2); $resourcelink2->ltiResourceLinkId = $ltiresourcelinkid . '2'; $resourcelink2->save(); $dc->loadResourceLink($resourcelink2); $this->assertNotEquals($resourcelink, $resourcelink2); } /** * Test for data_connector::saveResourceLink(). */ public function test_save_resource_link(): void { $dc = new data_connector(); // Consumer for the resource link. $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); // Context for the resource link. $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Save the context. $context->save(); $ltiresourcelinkid = 'testltiresourcelinkid'; $resourcelink = ResourceLink::fromConsumer($consumer, $ltiresourcelinkid); $resourcelink->setContextId($context->getRecordId()); $resourcelink->setSettings($settings); $resourcelink->shareApproved = true; $resourcelink->primaryResourceLinkId = 999; // Try to load an unsaved resource link. $this->assertFalse($dc->loadResourceLink($resourcelink)); // Save the resource link. $this->assertTrue($resourcelink->save()); // Check values. $resoucelinkid = $resourcelink->getRecordId(); $created = $resourcelink->created; $updated = $resourcelink->updated; $this->assertNotEmpty($resoucelinkid); $this->assertEquals($settings, $resourcelink->getSettings()); $this->assertTrue($resourcelink->shareApproved); $this->assertEquals(999, $resourcelink->primaryResourceLinkId); $this->assertNotEmpty($created); $this->assertNotEmpty($updated); // Update values. $newsettings = array_merge($settings, ['d', 'e']); $resourcelink->setSettings($newsettings); $resourcelink->shareApproved = false; $resourcelink->primaryResourceLinkId = 1000; $resourcelink->ltiResourceLinkId = $ltiresourcelinkid . 'edited'; // Save modified resource link. $this->assertTrue($resourcelink->save()); // Check edited values. $this->assertEquals($resoucelinkid, $resourcelink->getRecordId()); $this->assertEquals($newsettings, $resourcelink->getSettings()); $this->assertFalse($resourcelink->shareApproved); $this->assertEquals(1000, $resourcelink->primaryResourceLinkId); $this->assertEquals($created, $resourcelink->created); $this->assertGreaterThanOrEqual($updated, $resourcelink->updated); } /** * Test for data_connector::deleteResourceLink(). */ public function test_delete_resource_link(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; // Save the context. $this->assertTrue($dc->saveContext($context)); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->setContextId($context->getRecordId()); $resourcelink->save(); $resourcelinkchild = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid'); $resourcelinkchild->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild->shareApproved = true; $resourcelinkchild->setContextId($context->getRecordId()); $resourcelinkchild->save(); $resourcelinksharekey = new ResourceLinkShareKey($resourcelink); $resourcelinksharekey->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; $dc->saveUser($user); $this->assertTrue($dc->deleteResourceLink($resourcelink)); // Resource link properties should have been reset. $this->assertEmpty($resourcelink->title); $this->assertEmpty($resourcelink->getSettings()); $this->assertNull($resourcelink->groupSets); $this->assertNull($resourcelink->groups); $this->assertNull($resourcelink->primaryResourceLinkId); $this->assertNull($resourcelink->shareApproved); $this->assertNull($resourcelink->created); $this->assertNull($resourcelink->updated); // Share key record record should have been deleted. $this->assertFalse($dc->loadResourceLinkShareKey($resourcelinksharekey)); // Resource link record should have been deleted. $this->assertFalse($dc->loadResourceLink($resourcelink)); // Child resource link should still exist and its primaryResourceLinkId attribute should have been set to null. $this->assertTrue($dc->loadResourceLink($resourcelinkchild)); $this->assertNull($resourcelinkchild->primaryResourceLinkId); } /** * Test for data_connector::getUserResultSourcedIDsResourceLink(). */ public function test_get_user_result_sourced_ids_resource_link(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->idScope = ToolProvider::ID_SCOPE_GLOBAL; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; $context->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->setContextId($context->getRecordId()); $resourcelink->save(); $resourcelinkchild = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid'); $resourcelinkchild->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild->shareApproved = true; $resourcelinkchild->setContextId($context->getRecordId()); $resourcelinkchild->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; $user->ltiUserId = 'user1'; $dc->saveUser($user); $user2 = User::fromResourceLink($resourcelinkchild, ''); $user2->ltiResultSourcedId = 'testLtiResultSourcedId2'; $user->ltiUserId = 'user2'; $dc->saveUser($user2); $users = $dc->getUserResultSourcedIDsResourceLink($resourcelink, false, null); $this->assertNotEmpty($users); $this->assertCount(2, $users); $users = $dc->getUserResultSourcedIDsResourceLink($resourcelink, true, null); $this->assertNotEmpty($users); $this->assertCount(1, $users); $users = $dc->getUserResultSourcedIDsResourceLink($resourcelink, false, ToolProvider::ID_SCOPE_GLOBAL); $this->assertNotEmpty($users); $this->assertCount(2, $users); $users = $dc->getUserResultSourcedIDsResourceLink($resourcelink, true, ToolProvider::ID_SCOPE_GLOBAL); $this->assertNotEmpty($users); $this->assertCount(1, $users); } /** * Test for data_connector::getSharesResourceLink(). */ public function test_get_shares_resource_link(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->idScope = ToolProvider::ID_SCOPE_GLOBAL; $consumer->save(); $title = 'testcontexttitle'; $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->title = $title; $context->settings = $settings; $context->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->setContextId($context->getRecordId()); $resourcelink->save(); $shares = $dc->getSharesResourceLink($resourcelink); $this->assertEmpty($shares); $resourcelinkchild = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid'); $resourcelinkchild->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild->shareApproved = true; $resourcelinkchild->save(); $resourcelinkchild2 = ResourceLink::fromConsumer($consumer, 'testresourcelinkchildid2'); $resourcelinkchild2->primaryResourceLinkId = $resourcelink->getRecordId(); $resourcelinkchild2->shareApproved = false; $resourcelinkchild2->save(); $shares = $dc->getSharesResourceLink($resourcelink); $this->assertCount(2, $shares); $shareids = [$resourcelinkchild->getRecordId(), $resourcelinkchild2->getRecordId()]; foreach ($shares as $share) { $this->assertTrue($share instanceof ResourceLinkShare); $this->assertTrue(in_array($share->resourceLinkId, $shareids)); if ($share->resourceLinkId == $shareids[0]) { $this->assertTrue($share->approved); } else { $this->assertFalse($share->approved); } } } /** * Test for data_connector::loadConsumerNonce(). */ public function test_load_consumer_nonce(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $nonce = new ConsumerNonce($consumer, 'testnonce'); // Should still not be available since it has not been saved yet. $this->assertFalse($dc->loadConsumerNonce($nonce)); // Save the nonce. $nonce->save(); // Should now be available. $this->assertTrue($dc->loadConsumerNonce($nonce)); } /** * Test for data_connector::loadConsumerNonce() for a nonce that has expired. */ public function test_load_consumer_nonce_expired(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $nonce = new ConsumerNonce($consumer, 'testnonce'); $nonce->expires = time() - 100; // Save the nonce. $nonce->save(); // Expired nonce should have been deleted. $this->assertFalse($dc->loadConsumerNonce($nonce)); } /** * Test for data_connector::saveConsumerNonce(). */ public function test_save_consumer_nonce(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $nonce = new ConsumerNonce($consumer, 'testnonce'); // Save the nonce. $this->assertTrue($dc->saveConsumerNonce($nonce)); } /** * Test for data_connector::loadResourceLinkShareKey(). */ public function test_load_resource_link_share_key(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $sharekey = new ResourceLinkShareKey($resourcelink); // Should still not be available since it has not been saved yet. $this->assertFalse($dc->loadResourceLinkShareKey($sharekey)); // Save the share key. $sharekey->save(); // Should now be available. $this->assertTrue($dc->loadResourceLinkShareKey($sharekey)); // Check values. $this->assertEquals(strlen($sharekey->getId()), $sharekey->length); $this->assertEquals(ResourceLinkShareKey::DEFAULT_SHARE_KEY_LIFE, $sharekey->life); $this->assertNotNull($sharekey->expires); $this->assertFalse($sharekey->autoApprove); } /** * Test for data_connector::loadResourceLinkShareKey() with an expired share key. */ public function test_load_resource_link_share_key_expired(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $sharekey = new ResourceLinkShareKey($resourcelink, 'testsharelinkid'); $sharekey->expires = time() - 100; // ResourceLinkShareKey::save() adds a default expires time and cannot be modified. $dc->saveResourceLinkShareKey($sharekey); // Expired shared key should have been deleted. $this->assertFalse($dc->loadResourceLinkShareKey($sharekey)); } /** * Test for data_connector::saveResourceLinkShareKey(). */ public function test_save_resource_link_share_key(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $expires = time() - 100; $sharekey = new ResourceLinkShareKey($resourcelink, 'testsharelinkid'); $sharekey->expires = $expires; $sharekey->life = ResourceLinkShareKey::DEFAULT_SHARE_KEY_LIFE; $this->assertTrue($dc->saveResourceLinkShareKey($sharekey)); // Check values. $this->assertEquals(strlen($sharekey->getId()), $sharekey->length); $this->assertEquals(ResourceLinkShareKey::DEFAULT_SHARE_KEY_LIFE, $sharekey->life); $this->assertEquals($expires, $sharekey->expires); $this->assertFalse($sharekey->autoApprove); } /** * Test for data_connector::deleteResourceLinkShareKey(). */ public function test_delete_resource_link_share_key(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $sharekey = new ResourceLinkShareKey($resourcelink, 'testsharelinkid'); $sharekey->save(); $this->assertTrue($dc->deleteResourceLinkShareKey($sharekey)); $controlsharekey = new ResourceLinkShareKey($resourcelink, 'testsharelinkid'); $controlsharekey->initialise(); $this->assertEquals($controlsharekey, $sharekey); // This should no longer be in the DB. $this->assertFalse($dc->loadResourceLinkShareKey($sharekey)); } /** * Test for data_connector::loadUser(). */ public function test_load_user(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; // Should still not be available since it has not been saved yet. $this->assertFalse($dc->loadUser($user)); // Save the user. $user->save(); // Should now be available. $this->assertTrue($dc->loadUser($user)); // Check loaded values. $created = $user->created; $updated = $user->updated; $this->assertNotNull($created); $this->assertNotNull($updated); $this->assertEquals('testLtiResultSourcedId', $user->ltiResultSourcedId); $this->assertEquals($resourcelink, $user->getResourceLink()); } /** * Test for data_connector::saveUser(). */ public function test_save_user(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; // Save user. $this->assertTrue($dc->saveUser($user)); // Check loaded values. $created = $user->created; $updated = $user->updated; $this->assertNotNull($created); $this->assertNotNull($updated); $this->assertEquals('testLtiResultSourcedId', $user->ltiResultSourcedId); $this->assertEquals($resourcelink, $user->getResourceLink()); // Update user. $user->ltiResultSourcedId = 'testLtiResultSourcedId2'; // Save updated values. $this->assertTrue($dc->saveUser($user)); // Check updated values. $this->assertEquals($created, $user->created); $this->assertGreaterThanOrEqual($updated, $user->updated); $this->assertEquals('testLtiResultSourcedId2', $user->ltiResultSourcedId); } /** * Test for data_connector::deleteUser(). */ public function test_delete_user(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $user = User::fromResourceLink($resourcelink, ''); $user->ltiResultSourcedId = 'testLtiResultSourcedId'; $user->firstname = 'First'; $user->lastname = 'Last'; $user->fullname = 'Full name'; $user->email = 'test@email.com'; $user->roles = ['a', 'b']; $user->groups = ['1', '2']; $user->save(); // Delete user. $this->assertTrue($dc->deleteUser($user)); // User record should have been deleted from the DB. $this->assertFalse($dc->loadUser($user)); // User object should have been initialised(). $this->assertEmpty($user->firstname); $this->assertEmpty($user->lastname); $this->assertEmpty($user->fullname); $this->assertEmpty($user->email); $this->assertEmpty($user->roles); $this->assertEmpty($user->groups); $this->assertNull($user->ltiResultSourcedId); $this->assertNull($user->created); $this->assertNull($user->updated); } /** * Test for data_connector::get_contexts_from_consumer(). */ public function test_get_contexts_from_consumer(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'testconsumername'; $consumer->setKey('TestKey'); $consumer->secret = 'testsecret'; $consumer->save(); $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->settings = $settings; $context->save(); $dc->loadContext($context); $consumer2 = new ToolConsumer(null, $dc); $consumer2->name = 'testconsumername2'; $consumer2->setKey('TestKey2'); $consumer2->secret = 'testsecret2'; $consumer2->save(); $context2 = Context::fromConsumer($consumer2, $lticontextid . '2'); $context2->settings = $settings; $consumer2->save(); $contexts = $dc->get_contexts_from_consumer($consumer); $this->assertCount(1, $contexts); $this->assertEquals($context, $contexts[0]); } /** * Test for data_connector::get_consumers_mapped_to_tool(). */ public function test_get_consumers_mapped_to_tool(): void { $generator = $this->getDataGenerator(); // Create two tools belonging to the same course. $course1 = $generator->create_course(); $data = new \stdClass(); $data->courseid = $course1->id; $tool = $generator->create_lti_tool($data); $tool2 = $generator->create_lti_tool($data); $dc = new data_connector(); $consumer = new ToolConsumer('key1', $dc); $consumer->name = 'testconsumername'; $consumer->secret = 'testsecret'; $consumer->save(); $tp = new \enrol_lti\tool_provider($tool->id); $tp->consumer = $consumer; $tp->map_tool_to_consumer(); $consumer2 = new ToolConsumer('key2', $dc); $consumer2->name = 'testconsumername2'; $consumer2->secret = 'testsecret2'; $consumer2->save(); $tp2 = new \enrol_lti\tool_provider($tool2->id); $tp2->consumer = $consumer2; $tp2->map_tool_to_consumer(); $consumers = $dc->get_consumers_mapped_to_tool($tool->id); $this->assertCount(1, $consumers); $this->assertEquals($consumer, $consumers[0]); $consumers2 = $dc->get_consumers_mapped_to_tool($tool2->id); $this->assertCount(1, $consumers2); $this->assertEquals($consumer2, $consumers2[0]); } /** * Test for data_connector::get_resourcelink_from_consumer() */ public function test_get_resourcelink_from_consumer(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); // No ResourceLink associated with the ToolConsumer yet. $this->assertNull($dc->get_resourcelink_from_consumer($consumer)); // Create and save ResourceLink from ToolConsumer. $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); $resourcelink->save(); $dc->loadResourceLink($resourcelink); // Assert that the resource link and the one fetched by get_resourcelink_from_consumer() are the same. $this->assertEquals($resourcelink, $dc->get_resourcelink_from_consumer($consumer)); } /** * Test for data_connector::get_resourcelink_from_context() */ public function test_get_resourcelink_from_context(): void { $dc = new data_connector(); $consumer = new ToolConsumer(null, $dc); $consumer->name = 'TestName'; $consumer->setKey('TestKey'); $consumer->secret = 'TestSecret'; $consumer->save(); $settings = ['a', 'b', 'c']; $lticontextid = 'testlticontextid'; $context = Context::fromConsumer($consumer, $lticontextid); $context->settings = $settings; $context->save(); // No ResourceLink associated with the Context yet. $this->assertNull($dc->get_resourcelink_from_context($context)); // Create and save ResourceLink from the Context. $resourcelink = ResourceLink::fromContext($context, 'testresourcelinkid'); $resourcelink->save(); $dc->loadResourceLink($resourcelink); // Assert that the resource link and the one fetched by get_resourcelink_from_context() are the same. $this->assertEquals($resourcelink, $dc->get_resourcelink_from_context($context)); } } tests/sync_members_test.php000064400000031351152266447110012157 0ustar00. /** * Tests for the sync_members scheduled task class. * * @package enrol_lti * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti; use enrol_lti\task\sync_members; use IMSGlobal\LTI\ToolProvider\Context; use IMSGlobal\LTI\ToolProvider\ResourceLink; use IMSGlobal\LTI\ToolProvider\ToolConsumer; use IMSGlobal\LTI\ToolProvider\User; defined('MOODLE_INTERNAL') || die(); /** * Tests for the sync_members scheduled task class. * * @package enrol_lti * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class sync_members_test extends \advanced_testcase { /** @var dummy_sync_members_task $task */ protected $task; /** @var \stdClass $tool The published tool. */ protected $tool; /** @var User[] $members */ protected $members; /** @var ToolConsumer $consumer */ protected $consumer; /** @var Context $context */ protected $context; /** @var ResourceLink $resourcelink */ protected $resourcelink; public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Set this user as the admin. $this->setAdminUser(); $this->task = new dummy_sync_members_task(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $tooldata = [ 'courseid' => $course->id, 'membersyncmode' => helper::MEMBER_SYNC_ENROL_AND_UNENROL, 'membersync' => 1, ]; $tool = $generator->create_lti_tool((object)$tooldata); $this->tool = helper::get_lti_tool($tool->id); $dataconnector = $this->task->get_dataconnector(); $this->consumer = new ToolConsumer('Consumer1Key', $dataconnector); $this->consumer->name = 'Consumer1'; $this->consumer->secret = 'Consumer1Secret'; $this->consumer->save(); $toolprovider = new tool_provider($this->tool->id); $toolprovider->consumer = $this->consumer; $toolprovider->map_tool_to_consumer(); $imageurl = $this->getExternalTestFileUrl('test.jpg'); $count = 10; $this->members = []; for ($i = 1; $i <= $count; $i++) { $user = new User(); $user->firstname = 'Firstname' . $i; $user->lastname = 'Lastname' . $i; $user->ltiUserId = 'user' . $i; // Set user image values for some users. if ($i % 3 == 0) { $user->image = $imageurl; } $this->members[] = $user; } $this->context = Context::fromConsumer($this->consumer, 'testlticontextid'); $this->context->save(); $this->resourcelink = ResourceLink::fromContext($this->context, 'testresourcelinkid'); $this->resourcelink->save(); } /** * Test for sync_members::do_context_membership_request(). */ public function test_do_context_membership_request(): void { // Suppress output. ob_start(); $members = $this->task->do_context_membership_request($this->context); ob_end_clean(); $this->assertFalse($members); } /** * Test for sync_members::do_resourcelink_membership_request(). */ public function test_do_resourcelink_membership_request(): void { $members = $this->task->do_resourcelink_membership_request($this->resourcelink); $this->assertFalse($members); } /** * Test for sync_members::execute() when auth_lti is disabled. */ public function test_execute_authdisabled(): void { ob_start(); $this->task->execute(); $output = ob_get_clean(); $message = 'Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')); $this->assertStringContainsString($message, $output); } /** * Test for sync_members::execute() when enrol_lti is disabled. */ public function test_execute_enroldisabled(): void { // Enable auth_lti. $this->enable_auth(); ob_start(); $this->task->execute(); $output = ob_get_clean(); $message = 'Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'); $this->assertStringContainsString($message, $output); } /** * Test for sync_members::execute(). */ public function test_execute(): void { // Enable auth_lti. $this->enable_auth(); // Enable enrol_lti. $this->enable_enrol(); ob_start(); $this->task->execute(); $output = ob_get_clean(); $membersyncmessage = "Completed - Synced members for tool '{$this->tool->id}' in the course '{$this->tool->courseid}'"; $this->assertStringContainsString($membersyncmessage, $output); $imagesyncmessage = "Completed - Synced 0 profile images."; $this->assertStringContainsString($imagesyncmessage, $output); } /** * Test for sync_members::fetch_members_from_consumer() with no resource link nor context associated with the consumer. */ public function test_fetch_members_from_consumer_noresourcelink_nocontext(): void { // Suppress output. ob_start(); $members = $this->task->fetch_members_from_consumer($this->consumer); ob_end_clean(); $this->assertFalse($members); } /** * Test for sync_members::get_name(). */ public function test_get_name(): void { $this->assertEquals(get_string('tasksyncmembers', 'enrol_lti'), $this->task->get_name()); } /** * Test for sync_members::should_sync_enrol(). */ public function test_should_sync_enrol(): void { $this->assertTrue($this->task->should_sync_enrol(helper::MEMBER_SYNC_ENROL_AND_UNENROL)); $this->assertTrue($this->task->should_sync_enrol(helper::MEMBER_SYNC_ENROL_NEW)); $this->assertFalse($this->task->should_sync_enrol(helper::MEMBER_SYNC_UNENROL_MISSING)); } /** * Test for sync_members::should_sync_unenrol(). */ public function test_should_sync_unenrol(): void { $this->assertTrue($this->task->should_sync_unenrol(helper::MEMBER_SYNC_ENROL_AND_UNENROL)); $this->assertFalse($this->task->should_sync_unenrol(helper::MEMBER_SYNC_ENROL_NEW)); $this->assertTrue($this->task->should_sync_unenrol(helper::MEMBER_SYNC_UNENROL_MISSING)); } /** * Test for sync_members::sync_member_information(). */ public function test_sync_member_information(): void { list($users, $enrolledcount) = $this->task->sync_member_information($this->tool, $this->consumer, $this->members); $membercount = count($this->members); $this->assertCount(10, $this->members); $this->assertCount($membercount, $users); $this->assertEquals($membercount, $enrolledcount); } /** * Test for sync_members::sync_profile_images(). */ public function test_sync_profile_images(): void { $task = $this->task; list($users, $enrolledcount) = $task->sync_member_information($this->tool, $this->consumer, $this->members); $membercount = count($this->members); $this->assertCount(10, $this->members); $this->assertCount($membercount, $users); $this->assertEquals($membercount, $enrolledcount); // Suppress output. ob_start(); $this->assertEquals(3, $task->sync_profile_images()); ob_end_clean(); } /** * Test for sync_members::sync_unenrol(). */ public function test_sync_unenrol(): void { $tool = $this->tool; $task = $this->task; list($users) = $task->sync_member_information($tool, $this->consumer, $this->members); // Simulate that the fetched list of current users has been reduced by 3. $unenrolcount = 3; for ($i = 0; $i < $unenrolcount; $i++) { array_pop($users); } $this->assertEquals($unenrolcount, $task->sync_unenrol($tool, 'Consumer1Key', $users)); } /** * Enable auth_lti plugin. */ protected function enable_auth() { $auths = get_enabled_auth_plugins(); if (!in_array('lti', $auths)) { $auths[] = 'lti'; } set_config('auth', implode(',', $auths)); } /** * Enable enrol_lti plugin. */ protected function enable_enrol() { $enabled = enrol_get_plugins(true); $enabled['lti'] = true; $enabled = array_keys($enabled); set_config('enrol_plugins_enabled', implode(',', $enabled)); } } /** * Class dummy_sync_members_task. * * A class that extends sync_members so that we can expose the protected methods that we would like to test. * * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dummy_sync_members_task extends sync_members { /** * Exposes/generates the dataconnector property. * * @return data_connector */ public function get_dataconnector() { if (!$this->dataconnector) { $this->dataconnector = new data_connector(); } return $this->dataconnector; } /** * Exposes sync_members::do_context_membership_request() * * @param Context $context The context object. * @param ResourceLink $resourcelink The resource link object. * @param string $membershipsurltemplate The memberships endpoint URL template. * @return bool|User[] Array of User objects upon successful membership service request. False, otherwise. */ public function do_context_membership_request(Context $context, ?ResourceLink $resourcelink = null, $membershipsurltemplate = '') { $members = parent::do_context_membership_request($context, $resourcelink, $membershipsurltemplate); return $members; } /** * Exposes sync_members::do_resourcelink_membership_request() * * @param ResourceLink $resourcelink * @return bool|User[] */ public function do_resourcelink_membership_request(ResourceLink $resourcelink) { $members = parent::do_resourcelink_membership_request($resourcelink); return $members; } /** * Exposes sync_members::fetch_members_from_consumer() * * @param ToolConsumer $consumer * @return bool|User[] */ public function fetch_members_from_consumer(ToolConsumer $consumer) { $members = parent::fetch_members_from_consumer($consumer); return $members; } /** * Exposes sync_members::should_sync_unenrol() * * @param int $syncmode The tool's membersyncmode. * @return bool */ public function should_sync_unenrol($syncmode) { $shouldsync = parent::should_sync_unenrol($syncmode); return $shouldsync; } /** * Exposes sync_members::should_sync_enrol() * * @param int $syncmode The tool's membersyncmode. * @return bool */ public function should_sync_enrol($syncmode) { $shouldsync = parent::should_sync_enrol($syncmode); return $shouldsync; } /** * Exposes sync_members::sync_member_information() * * @param \stdClass $tool * @param ToolConsumer $consumer * @param User[] $members * @return array */ public function sync_member_information(\stdClass $tool, ToolConsumer $consumer, $members) { $result = parent::sync_member_information($tool, $consumer, $members); return $result; } /** * Exposes sync_members::sync_profile_images() * * @return int */ public function sync_profile_images() { $count = parent::sync_profile_images(); return $count; } /** * Exposes sync_members::sync_unenrol() * * @param \stdClass $tool * @param string $consumerkey * @param array $currentusers * @return int */ public function sync_unenrol(\stdClass $tool, string $consumerkey, array $currentusers) { $count = parent::sync_unenrol($tool, $consumerkey, $currentusers); return $count; } } tests/behat/lti_adv_tool_registration.feature000064400000032467152266447110015643 0ustar00@enrol @enrol_lti Feature: Register a platform with the tool In order to share and consume a Moodle resource or activity over LTI Advantage As an admin I need to be able to manage platform registrations in the tool Background: Given I enable "lti" "enrol" plugin Scenario: An admin can register a platform with the tool Given I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I follow "Register a platform" And I set the following fields to these values: | Platform name | My test platform | And I press "Continue" And I should see "Dynamic registration" And I should see "Manual registration" And I should see "Platform details" And I follow "Back" And "Manage deployments" "link" should exist in the "My test platform" "table_row" And "View platform details" "link" should exist in the "My test platform" "table_row" And "Delete" "link" should exist in the "My test platform" "table_row" And I should see "Pending" in the "My test platform" "table_row" And I click on "View platform details" "link" in the "My test platform" "table_row" And I follow "Edit platform details" And I set the following fields to these values: | Platform ID (issuer) | https://lms.example.com | | Client ID | abcd1234 | | Authentication request URL | https://lms.example.com/auth | | Public keyset URL | https://lms.example.com/jwks | | Access token URL | https://lms.example.com/token | And I press "Save changes" Then I should see "Platform registration updated" And I should see "My test platform" in the "Platform name" "table_row" And I should see "https://lms.example.com" in the "Platform ID (issuer)" "table_row" And I should see "abcd1234" in the "Client ID" "table_row" And I should see "https://lms.example.com/auth" in the "Authentication request URL" "table_row" And I should see "https://lms.example.com/jwks" in the "Public keyset URL" "table_row" And I should see "https://lms.example.com/token" in the "Access token URL" "table_row" And I follow "Back" And I should see "https://lms.example.com" in the "My test platform" "table_row" And I should see "Active" in the "My test platform" "table_row" And "View platform details" "link" should exist in the "My test platform" "table_row" And "Manage deployments" "link" should exist in the "My test platform" "table_row" And "Delete" "link" should exist in the "My test platform" "table_row" Scenario: An admin can edit a platform's registration details Given the following "enrol_lti > application registrations" exist: | name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | | My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | And I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I click on "View platform details" "link" in the "My test platform" "table_row" And I follow "Edit platform details" And I set the following fields to these values: | Platform name | Changed test platform | | Platform ID (issuer) | https://lms2.example.com | | Client ID | wxyz9876 | | Authentication request URL | https://lms2.example.com/auth | | Public keyset URL | https://lms2.example.com/jwks | | Access token URL | https://lms2.example.com/token | And I press "Cancel" Then I should see "https://lms.example.com" in the "Platform ID (issuer)" "table_row" And I follow "Edit platform details" And the following fields match these values: | Platform name | My test platform | | Platform ID (issuer) | https://lms.example.com | | Client ID | abcd1234 | | Authentication request URL | https://lms.example.com/auth | | Public keyset URL | https://lms.example.com/jwks | | Access token URL | https://lms.example.com/token | And I set the following fields to these values: | Platform name | Changed test platform | | Platform ID (issuer) | https://lms2.example.com | | Client ID | wxyz9876 | | Authentication request URL | https://lms2.example.com/auth | | Public keyset URL | https://lms2.example.com/jwks | | Access token URL | https://lms2.example.com/token | And I press "Save changes" And I should see "https://lms2.example.com" in the "Platform ID (issuer)" "table_row" And I follow "Edit platform details" And the following fields match these values: | Platform name | Changed test platform | | Platform ID (issuer) | https://lms2.example.com | | Client ID | wxyz9876 | | Authentication request URL | https://lms2.example.com/auth | | Public keyset URL | https://lms2.example.com/jwks | | Access token URL | https://lms2.example.com/token | And I press "Cancel" And I follow "Back" And I should see "https://lms2.example.com" in the "Changed test platform" "table_row" Scenario: An admin can delete a platform registration Given the following "enrol_lti > application registrations" exist: | name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | | My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | And I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I click on "Delete" "link" in the "My test platform" "table_row" And I press "Cancel" Then I should see "https://lms.example.com" in the "My test platform" "table_row" And I click on "Delete" "link" in the "My test platform" "table_row" And I press "Continue" And I should see "Platform registration deleted" And I should not see "My test platform" Scenario: A platform registration's unique platformid:clientid tuple must be enforced Given the following "enrol_lti > application registrations" exist: | name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | | My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | And I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I follow "Register a platform" And I set the following fields to these values: | Platform name | My test platform | And I press "Continue" And I follow "Edit platform details" And I set the following fields to these values: | Platform name | My test platform | | Platform ID (issuer) | https://lms.example.com | | Client ID | abcd1234 | | Authentication request URL | https://lms.example.com/auth | | Public keyset URL | https://lms.example.com/jwks | | Access token URL | https://lms.example.com/token | And I press "Save changes" Then I should see "Invalid client ID. This client ID is already registered for the platform ID provided." Scenario: An admin can add deployment ids for a given platform registration Given the following "enrol_lti > application registrations" exist: | name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | | My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | And I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I click on "Manage deployments" "link" in the "My test platform" "table_row" And I should see "No tool deployments found" And I follow "Add a deployment" And I set the following fields to these values: | Deployment name | Sitewide deployment of Moodle on platform x | | Deployment ID | 1a2b3c | And I press "Cancel" And I should not see "Sitewide deployment of Moodle on platform x" And I should see "No tool deployments found" And I follow "Add a deployment" And I set the following fields to these values: | Deployment name | Sitewide deployment of Moodle on platform x | | Deployment ID | 1a2b3c | And I press "Save changes" Then I should see "Deployment added" And I should see "1a2b3c" in the "Sitewide deployment of Moodle on platform x" "table_row" And I should not see "No tool deployments found" And "Delete" "link" should exist in the "Sitewide deployment of Moodle on platform x" "table_row" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration And "1" "link" should exist in the "My test platform" "table_row" And I click on "1" "link" in the "My test platform" "table_row" And I should see "1a2b3c" in the "Sitewide deployment of Moodle on platform x" "table_row" And I follow "Add a deployment" And I set the following fields to these values: | Deployment name | Course context deployment of Moodle on platform x | | Deployment ID | 4d5e6f | And I press "Save changes" And I should see "4d5e6f" in the "Course context deployment of Moodle on platform x" "table_row" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration And "2" "link" should exist in the "My test platform" "table_row" Scenario: An admin can remove a deployment id for a platform registration Given the following "enrol_lti > application registrations" exist: | name | platformid | clientid | authrequesturl | jwksurl | accesstokenurl | deploymentname | deploymentid | | My test platform | https://lms.example.com | abcd1234 | https://lms.example.com/auth | https://lms.example.com/jwks | https://lms.example.com/token | Site deployment | 1a2b3c | And I log in as "admin" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration And I should see "1" in the "My test platform" "table_row" And I click on "Manage deployments" "link" in the "My test platform" "table_row" And I should see "1a2b3c" in the "Site deployment" "table_row" When I click on "Delete" "link" in the "Site deployment" "table_row" And I press "Cancel" Then I should see "1a2b3c" in the "Site deployment" "table_row" And I click on "Delete" "link" in the "Site deployment" "table_row" And I press "Continue" And I should see "Deployment deleted" And I should see "No tool deployments found" And I should not see "1a2b3c" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration And "1" "link" should not exist in the "My test platform" "table_row" @javascript Scenario: An admin can copy the manual and dynamic registration endpoints to register the tool with the platform Given the following "enrol_lti > application registrations" exist: | name | | My test platform | And I log in as "admin" And I change window size to "large" And I navigate to "Plugins > Enrolments > Publish as LTI tool > Tool registration" in site administration When I click on "View platform details" "link" in the "My test platform" "table_row" And I follow "Tool details" And the "value" attribute of "Registration URL" "field" should contain "enrol/lti/register.php" And the "value" attribute of "Tool URL" "field" should contain "enrol/lti/launch.php" And the "value" attribute of "Initiate login URL" "field" should contain "enrol/lti/login.php" And the "value" attribute of "JWKS URL" "field" should contain "enrol/lti/jwks.php" And the "value" attribute of "Deep linking URL" "field" should contain "enrol/lti/launch_deeplink.php" And "Copy to clipboard" "button" should exist in the "Registration URL" "table_row" And "Copy to clipboard" "button" should exist in the "Tool URL" "table_row" And "Copy to clipboard" "button" should exist in the "Initiate login URL" "table_row" And "Copy to clipboard" "button" should exist in the "JWKS URL" "table_row" And "Copy to clipboard" "button" should exist in the "Deep linking URL" "table_row" When I click on "Copy to clipboard" "button" in the "Registration URL" "table_row" Then I should see "Registration URL copied to clipboard" And I click on "Copy to clipboard" "button" in the "Tool URL" "table_row" And I should see "Tool URL copied to clipboard" And I click on "Copy to clipboard" "button" in the "Initiate login URL" "table_row" And I should see "Initiate login URL copied to clipboard" And I click on "Copy to clipboard" "button" in the "JWKS URL" "table_row" And I should see "JWKS URL copied to clipboard" And I click on "Copy to clipboard" "button" in the "Deep linking URL" "table_row" And I should see "Deep linking URL copied to clipboard" tests/behat/basic_settings.feature000064400000005371152266447110013365 0ustar00@enrol @enrol_lti Feature: Check that settings are adhered to when creating an enrolment plugin In order to create an LTI enrolment instance As an admin I need to ensure the site-wide settings are used Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "admin" And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration And I click on "Enable" "link" in the "Publish as LTI tool" "table_row" And I navigate to "Plugins > Enrolments > Publish as LTI tool" in site administration And I set the following fields to these values: | Email visibility | Visible to everyone | | City/town | Perth | | Select a country | Australia | | Timezone | Australia/Perth | | Institution | Moodle Pty Ltd | And I press "Save changes" And I log out Scenario: As an admin set site-wide settings for the enrolment plugin and ensure they are used Given I log in as "teacher1" And I am on the "Course 1" "enrolment methods" page And I select "Publish as LTI tool" from the "Add method" singleselect When I expand all fieldsets Then the field "Email visibility" matches value "Visible to everyone" And the field "City/town" matches value "Perth" And the field "Select a country" matches value "Australia" And the field "Timezone" matches value "Australia/Perth" And the field "Institution" matches value "Moodle Pty Ltd" And I set the following fields to these values: | Email visibility | Hidden | | City/town | Whistler | | Select a country | Canada | | Timezone | America/Vancouver | | Institution | Moodle Pty Ltd - remote | And I press "Add method" And I click on "Edit" "link" in the "Publish as LTI tool" "table_row" And the field "Email visibility" matches value "Hidden" And the field "City/town" matches value "Whistler" And the field "Select a country" matches value "Canada" And the field "Timezone" matches value "America/Vancouver" And the field "Institution" matches value "Moodle Pty Ltd - remote" tests/behat/index_page.feature000064400000005370152266447110012466 0ustar00@enrol @enrol_lti Feature: Check that the page listing the shared external tools is functioning as expected In order to edit an external tool As a teacher I need to ensure the tool listing page is working as expected Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | assign | Test assignment name | Submit your online text | C1 | assign1 | 1 | And I log in as "admin" And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration And I click on "Enable" "link" in the "Publish as LTI tool" "table_row" And I log out Scenario: I want to edit an external tool Given I log in as "teacher1" And I turn editing mode on And I am on the "Course 1" "enrolment methods" page And I select "Publish as LTI tool" from the "Add method" singleselect And I set the following fields to these values: | Custom instance name | Assignment - LTI | | Tool to be published | Test assignment name | | LTI version | Legacy LTI (1.1/2.0) | And I press "Add method" And I am on "Course 1" course homepage And I navigate to "Published as LTI tools" in current page administration And I click on "Legacy LTI (1.1/2.0" "link" And I should see "Assignment - LTI" in the ".generaltable" "css_element" When I click on "Disable" "link" in the "Assignment - LTI" "table_row" Then ".dimmed_text" "css_element" should exist in the "Assignment - LTI" "table_row" And I click on "Enable" "link" in the "Assignment - LTI" "table_row" And ".dimmed_text" "css_element" should not exist in the "Assignment - LTI" "table_row" And I click on "Edit" "link" in the "Assignment - LTI" "table_row" And I set the following fields to these values: | Custom instance name | Course - LTI | | Tool to be published | Course | And I press "Save changes" And I should see "Course - LTI" in the ".generaltable" "css_element" And I click on "Delete" "link" in the "Course - LTI" "table_row" And I press "Cancel" And I should see "Course - LTI" in the ".generaltable" "css_element" And I click on "Delete" "link" in the "Course - LTI" "table_row" And I press "Continue" And I should see "No resources or activities are published yet" And I should not see "Course - LTI" tests/behat/lti_adv_resource_publication.feature000064400000013332152266447110016302 0ustar00@enrol @enrol_lti Feature: Publish activities and resources over LTI Advantage In order to make content available to external platforms As a teacher I need to be able to publish and manage activities and resources using LTI Advantage Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | assign | Test assignment name | Submit your online text | C1 | assign1 | 1 | And I enable "lti" "enrol" plugin Scenario: A teacher can publish an activity Given I log in as "teacher1" And I am on the "Course 1" "enrolment methods" page When I select "Publish as LTI tool" from the "Add method" singleselect And the following fields match these values: | LTI version | LTI Advantage | And I set the following fields to these values: | Custom instance name | Published assignment | | Tool to be published | Test assignment name | And I press "Add method" And I am on "Course 1" course homepage And I navigate to "Published as LTI tools" in current page administration Then I should see "Launch URL" in the "Published assignment" "table_row" And I should see "Custom properties" in the "Published assignment" "table_row" And "Edit" "link" should exist in the "Published assignment" "table_row" And "Delete" "link" should exist in the "Published assignment" "table_row" And "Disable" "link" should exist in the "Published assignment" "table_row" Scenario: A teacher can edit a published resource/activity Given the following "enrol_lti > published resources" exist: | name | uuid | activity | course | | Published assignment | my-uuid-123 | assign1 | C1 | And I am on the "Course 1" "Course" page logged in as "teacher1" And I navigate to "Published as LTI tools" in current page administration And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123" When I click on "Edit" "link" in the "Published assignment" "table_row" And I set the following fields to these values: | Custom instance name | New instance name | And I press "Cancel" And I should see "Published assignment" in the "Published assignment" "table_row" And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123" And I click on "Edit" "link" in the "Published assignment" "table_row" And I set the following fields to these values: | Custom instance name | New instance name | And I press "Save changes" Then I should see "New instance name" And the "value" attribute of "Custom properties" "field" should contain "id=my-uuid-123" Scenario: A teacher can disable and enable a published resource/activity Given the following "enrol_lti > published resources" exist: | name | activity | course | | Published assignment | assign1 | C1 | And I am on the "Course 1" "Course" page logged in as "teacher1" And I navigate to "Published as LTI tools" in current page administration When I click on "Disable" "link" in the "Published assignment" "table_row" Then ".dimmed_text" "css_element" should exist in the "Published assignment" "table_row" And I click on "Enable" "link" in the "Published assignment" "table_row" And ".dimmed_text" "css_element" should not exist in the "Published assignment" "table_row" Scenario: A teacher can delete a published tool Given the following "enrol_lti > published resources" exist: | name | activity | course | | Published assignment | assign1 | C1 | And I am on the "Course 1" "Course" page logged in as "teacher1" And I navigate to "Published as LTI tools" in current page administration When I click on "Delete" "link" in the "Published assignment" "table_row" And I press "Cancel" And I should see "Published assignment" in the "Published assignment" "table_row" And I click on "Delete" "link" in the "Published assignment" "table_row" And I press "Continue" And I should see "No resources or activities are published yet" And I should not see "Published assignment" Scenario: A teacher can switch the version of a published resource from LTI 1.1 to LTI Advantage Given the following "enrol_lti > published resources" exist: | name | activity | course | ltiversion | | Published assignment | assign1 | C1 | LTI-1p0/LTI-2p0 | And I am on the "Course 1" "Course" page logged in as "teacher1" And I navigate to "Published as LTI tools" in current page administration And I should see "No resources or activities are published yet" And I click on "Legacy LTI (1.1/2.0)" "link" And I should see "Published assignment" When I click on "Edit" "link" in the "Published assignment" "table_row" And the following fields match these values: | LTI version | Legacy LTI (1.1/2.0) | And I set the following fields to these values: | LTI version | LTI Advantage | | Custom instance name | New instance name | And I press "Save changes" And I click on "LTI Advantage" "link" Then I should see "New instance name" And "LTI Advantage" "link" should not exist And "Legacy LTI (1.1/2.0)" "link" should exist And the "value" attribute of "Custom properties" "field" should contain "id=" tests/tool_provider_test.php000064400000054147152266447110012370 0ustar00. /** * Tests for the tool_provider class. * * @package enrol_lti * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_lti; use core\session\manager; use IMSGlobal\LTI\HTTPMessage; use IMSGlobal\LTI\ToolProvider\ResourceLink; use IMSGlobal\LTI\ToolProvider\ToolConsumer; use IMSGlobal\LTI\ToolProvider\ToolProvider; use IMSGlobal\LTI\ToolProvider\User; defined('MOODLE_INTERNAL') || die(); /** * Tests for the tool_provider class. * * @package enrol_lti * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class tool_provider_test extends \advanced_testcase { /** * @var \stdClass $tool The LTI tool. */ protected $tool; /** * Test set up. * * This is executed before running any tests in this file. */ public function setUp(): void { global $SESSION; parent::setUp(); $this->resetAfterTest(); manager::init_empty_session(); // Set this user as the admin. $this->setAdminUser(); $data = new \stdClass(); $data->enrolstartdate = time(); $data->secret = 'secret'; $toolrecord = $this->getDataGenerator()->create_lti_tool($data); $this->tool = helper::get_lti_tool($toolrecord->id); $SESSION->notifications = []; } /** * Passing non-existent tool ID. */ public function test_constructor_with_non_existent_tool(): void { $this->expectException('dml_exception'); new tool_provider(-1); } /** * Constructor test. */ public function test_constructor(): void { global $CFG, $SITE; $tool = $this->tool; $tp = new tool_provider($tool->id); $this->assertNull($tp->consumer); $this->assertNull($tp->returnUrl); $this->assertNull($tp->resourceLink); $this->assertNull($tp->context); $this->assertNotNull($tp->dataConnector); $this->assertEquals('', $tp->defaultEmail); $this->assertEquals(ToolProvider::ID_SCOPE_ID_ONLY, $tp->idScope); $this->assertFalse($tp->allowSharing); $this->assertEquals(ToolProvider::CONNECTION_ERROR_MESSAGE, $tp->message); $this->assertNull($tp->reason); $this->assertEmpty($tp->details); $this->assertEquals($CFG->wwwroot, $tp->baseUrl); $this->assertNotNull($tp->vendor); $this->assertEquals($SITE->shortname, $tp->vendor->id); $this->assertEquals($SITE->fullname, $tp->vendor->name); $this->assertEquals($SITE->summary, $tp->vendor->description); $token = helper::generate_proxy_token($tool->id); $name = helper::get_name($tool); $description = helper::get_description($tool); $this->assertNotNull($tp->product); $this->assertEquals($token, $tp->product->id); $this->assertEquals($name, $tp->product->name); $this->assertEquals($description, $tp->product->description); $this->assertNotNull($tp->requiredServices); $this->assertEmpty($tp->optionalServices); $this->assertNotNull($tp->resourceHandlers); } /** * Test for handle request. */ public function test_handle_request_no_request_data(): void { $tool = $this->tool; $tp = new tool_provider($tool->id); // Tool provider object should have been created fine. OK flag should be fine for now. $this->assertTrue($tp->ok); // Call handleRequest but suppress output. ob_start(); $tp->handleRequest(); ob_end_clean(); // There's basically no request data submitted so OK flag should turn out false. $this->assertFalse($tp->ok); } /** * Test for tool_provider::onError(). */ public function test_on_error(): void { $tool = $this->tool; $tp = new dummy_tool_provider($tool->id); $message = "THIS IS AN ERROR!"; $tp->message = $message; $tp->onError(); $errormessage = get_string('failedrequest', 'enrol_lti', ['reason' => $message]); $this->assertStringContainsString($errormessage, $tp->get_error_output()); } /** * Test for tool_provider::onRegister() with no tool consumer set. */ public function test_on_register_no_consumer(): void { $tool = $this->tool; $tp = new dummy_tool_provider($tool->id); $tp->onRegister(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidtoolconsumer', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onRegister() without return URL. */ public function test_on_register_no_return_url(): void { $tool = $this->tool; $dataconnector = new data_connector(); $consumer = new ToolConsumer('testkey', $dataconnector); $consumer->ltiVersion = ToolProvider::LTI_VERSION2; $consumer->secret = $tool->secret; $consumer->name = 'TEST CONSUMER NAME'; $consumer->consumerName = 'TEST CONSUMER INSTANCE NAME'; $consumer->consumerGuid = 'TEST CONSUMER INSTANCE GUID'; $consumer->consumerVersion = 'TEST CONSUMER INFO VERSION'; $consumer->enabled = true; $consumer->protected = true; $consumer->save(); $tp = new dummy_tool_provider($tool->id); $tp->consumer = $consumer; $tp->onRegister(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('returnurlnotset', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onRegister() when registration fails. */ public function test_on_register_failed(): void { global $CFG; $tool = $this->tool; $dataconnector = new data_connector(); $consumer = new dummy_tool_consumer('testkey', $dataconnector); $consumer->ltiVersion = ToolProvider::LTI_VERSION2; $consumer->secret = $tool->secret; $consumer->name = 'TEST CONSUMER NAME'; $consumer->consumerName = 'TEST CONSUMER INSTANCE NAME'; $consumer->consumerGuid = 'TEST CONSUMER INSTANCE GUID'; $consumer->consumerVersion = 'TEST CONSUMER INFO VERSION'; $consumer->enabled = true; $consumer->protected = true; $profilejson = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'tool_consumer_profile.json')); $consumer->profile = json_decode($profilejson); $consumer->save(); $tp = new dummy_tool_provider($tool->id); $tp->consumer = $consumer; $tp->returnUrl = $CFG->wwwroot; $tp->onRegister(); // The OK flag will be false. $this->assertFalse($tp->ok); // Check message. $this->assertEquals(get_string('couldnotestablishproxy', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onRegister() when registration succeeds. */ public function test_on_register(): void { global $CFG, $DB; $tool = $this->tool; $dataconnector = new data_connector(); $consumer = new dummy_tool_consumer('testkey', $dataconnector, false, true); $consumer->ltiVersion = ToolProvider::LTI_VERSION2; $consumer->secret = $tool->secret; $consumer->name = 'TEST CONSUMER NAME'; $consumer->consumerName = 'TEST CONSUMER INSTANCE NAME'; $consumer->consumerGuid = 'TEST CONSUMER INSTANCE GUID'; $consumer->consumerVersion = 'TEST CONSUMER INFO VERSION'; $consumer->enabled = true; $consumer->protected = true; $profilejson = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'tool_consumer_profile.json')); $consumer->profile = json_decode($profilejson); $consumer->save(); $tp = new dummy_tool_provider($tool->id); $tp->consumer = $consumer; $tp->returnUrl = $CFG->wwwroot; // Capture output of onLaunch() method and save it as a string. ob_start(); $tp->onRegister(); $output = ob_get_clean(); $successmessage = get_string('successfulregistration', 'enrol_lti'); // Check output contents. Confirm that it has the success message and return URL. $this->assertStringContainsString($successmessage, $output); $this->assertStringContainsString($tp->returnUrl, $output); // The OK flag will be true on successful registration. $this->assertTrue($tp->ok); // Check tool provider message. $this->assertEquals($successmessage, $tp->message); // Check published tool and tool consumer mapping. $mappingparams = [ 'toolid' => $tool->id, 'consumerid' => $tp->consumer->getRecordId() ]; $this->assertTrue($DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams)); } /** * Test for tool_provider::onLaunch(). */ public function test_on_launch_no_frame_embedding(): void { $tp = $this->build_dummy_tp(); // Capture output of onLaunch() method and save it as a string. ob_start(); // Suppress session header errors. @$tp->onLaunch(); $output = ob_get_clean(); $this->assertStringContainsString(get_string('frameembeddingnotenabled', 'enrol_lti'), $output); } /** * Test for tool_provider::onLaunch(). */ public function test_on_launch_with_frame_embedding(): void { global $CFG; $CFG->allowframembedding = true; $tp = $this->build_dummy_tp(); // If redirect was called here, we will encounter an 'unsupported redirect error'. // We just want to verify that redirect() was called if frame embedding is allowed. $this->expectException('moodle_exception'); // Suppress session header errors. @$tp->onLaunch(); } /** * Test for tool_provider::onLaunch() with invalid secret and no tool proxy (for LTI 1 launches). */ public function test_on_launch_with_invalid_secret_and_no_proxy(): void { $tp = $this->build_dummy_tp('badsecret'); // Suppress session header errors. @$tp->onLaunch(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidrequest', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onLaunch() with invalid launch URL. */ public function test_on_launch_proxy_with_invalid_launch_url(): void { $proxy = [ 'tool_profile' => [ 'resource_handler' => [ [ 'message' => [ [ 'message_type' => 'basic-lti-launch-request', 'path' => '/enrol/lti/tool.php' ] ] ] ] ] ]; $tp = $this->build_dummy_tp($this->tool->secret, $proxy); // Suppress session header errors. @$tp->onLaunch(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidrequest', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onLaunch() with invalid launch URL. */ public function test_on_launch_proxy_with_valid_launch_url(): void { $tool = $this->tool; $proxy = [ 'tool_profile' => [ 'resource_handler' => [ [ 'message' => [ [ 'message_type' => 'basic-lti-launch-request', 'path' => '/enrol/lti/tool.php?id=' . $tool->id ] ] ] ] ] ]; $tp = $this->build_dummy_tp($this->tool->secret, $proxy); // Capture output of onLaunch() method and save it as a string. ob_start(); // Suppress session header errors. @$tp->onLaunch(); $output = ob_get_clean(); $this->assertTrue($tp->ok); $this->assertEquals(get_string('success'), $tp->message); $this->assertStringContainsString(get_string('frameembeddingnotenabled', 'enrol_lti'), $output); } /** * Test for tool_provider::onLaunch() for a request with message type other than basic-lti-launch-request. */ public function test_on_launch_proxy_with_invalid_message_type(): void { $tool = $this->tool; $proxy = [ 'tool_profile' => [ 'resource_handler' => [ [ 'message' => [ [ 'message_type' => 'ContentItemSelectionRequest', 'path' => '/enrol/lti/tool.php?id=' . $tool->id ] ] ] ] ] ]; $tp = $this->build_dummy_tp($this->tool->secret, $proxy); // Suppress session header errors. @$tp->onLaunch(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidrequest', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::onLaunch() to verify that a user image can be set from the resource link's custom_user_image setting. */ public function test_on_launch_with_user_image_from_resource_link(): void { global $DB; $userimageurl = $this->getExternalTestFileUrl('test.jpg'); $resourcelinksettings = [ 'custom_user_image' => $userimageurl ]; $tp = $this->build_dummy_tp($this->tool->secret, null, $resourcelinksettings); // Suppress output and session header errors. ob_start(); @$tp->onLaunch(); ob_end_clean(); $this->assertEquals($userimageurl, $tp->resourceLink->getSetting('custom_user_image')); $username = helper::create_username($tp->consumer->getKey(), $tp->user->ltiUserId); $user = $DB->get_record('user', ['username' => $username]); // User was found. $this->assertNotFalse($user); // User picture was set. $this->assertNotEmpty($user->picture); } /** * Test for tool_provider::onLaunch() to verify that a LTI user has been enrolled. */ public function test_on_launch_user_enrolment(): void { global $DB; $tp = $this->build_dummy_tp($this->tool->secret); // Suppress output and session header errors. ob_start(); @$tp->onLaunch(); ob_end_clean(); $username = helper::create_username($tp->consumer->getKey(), $tp->user->ltiUserId); $user = $DB->get_record('user', ['username' => $username]); // User was found. $this->assertNotFalse($user); // User picture was not set. $this->assertEmpty($user->picture); // Check user enrolment. $enrolled = $DB->record_exists('user_enrolments', ['enrolid' => $this->tool->enrolid, 'userid' => $user->id]); $this->assertTrue($enrolled); } /** * Test for tool_provider::onLaunch() when the consumer object has not been set. */ public function test_on_launch_no_consumer(): void { global $DB; $tool = $this->tool; $tp = new dummy_tool_provider($tool->id); $tp->onLaunch(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidtoolconsumer', 'enrol_lti'), $tp->message); // Check published tool and tool consumer has not yet been mapped due to failure. $mappingparams = [ 'toolid' => $tool->id ]; $this->assertFalse($DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams)); } /** * Test for tool_provider::onLaunch() when we have a non-existent consumer data. */ public function test_on_launch_invalid_consumer(): void { $tool = $this->tool; $dataconnector = new data_connector(); // Build consumer object but don't save it. $consumer = new dummy_tool_consumer('testkey', $dataconnector); $tp = new dummy_tool_provider($tool->id); $tp->consumer = $consumer; $tp->onLaunch(); $this->assertFalse($tp->ok); $this->assertEquals(get_string('invalidtoolconsumer', 'enrol_lti'), $tp->message); } /** * Test for tool_provider::map_tool_to_consumer(). */ public function test_map_tool_to_consumer(): void { global $DB; $tp = $this->build_dummy_tp(); $tp->map_tool_to_consumer(); // Check published tool and tool consumer mapping. $mappingparams = [ 'toolid' => $this->tool->id, 'consumerid' => $tp->consumer->getRecordId() ]; $this->assertTrue($DB->record_exists('enrol_lti_tool_consumer_map', $mappingparams)); } /** * Test for tool_provider::map_tool_to_consumer(). */ public function test_map_tool_to_consumer_no_consumer(): void { $tp = new dummy_tool_provider($this->tool->id); $this->expectException('moodle_exception'); $tp->map_tool_to_consumer(); } /** * Builds a dummy tool provider object. * * @param string $secret Consumer secret. * @param array|\stdClass $proxy Tool proxy data. * @param null $resourcelinksettings Key-value array for resource link settings. * @return dummy_tool_provider */ protected function build_dummy_tp($secret = null, $proxy = null, $resourcelinksettings = null) { $tool = $this->tool; $dataconnector = new data_connector(); $consumer = new ToolConsumer('testkey', $dataconnector); $ltiversion = ToolProvider::LTI_VERSION2; if ($secret === null && $proxy === null) { $consumer->secret = $tool->secret; $ltiversion = ToolProvider::LTI_VERSION1; } else { $consumer->secret = $secret; } $consumer->ltiVersion = $ltiversion; $consumer->name = 'TEST CONSUMER NAME'; $consumer->consumerName = 'TEST CONSUMER INSTANCE NAME'; $consumer->consumerGuid = 'TEST CONSUMER INSTANCE GUID'; $consumer->consumerVersion = 'TEST CONSUMER INFO VERSION'; $consumer->enabled = true; $consumer->protected = true; if ($proxy !== null) { $consumer->toolProxy = json_encode($proxy); } $consumer->save(); $resourcelink = ResourceLink::fromConsumer($consumer, 'testresourcelinkid'); if (!empty($resourcelinksettings)) { foreach ($resourcelinksettings as $setting => $value) { $resourcelink->setSetting($setting, $value); } } $resourcelink->save(); $ltiuser = User::fromResourceLink($resourcelink, ''); $ltiuser->ltiResultSourcedId = 'testLtiResultSourcedId'; $ltiuser->ltiUserId = 'testuserid'; $ltiuser->email = 'user1@example.com'; $ltiuser->save(); $tp = new dummy_tool_provider($tool->id); $tp->user = $ltiuser; $tp->resourceLink = $resourcelink; $tp->consumer = $consumer; return $tp; } } /** * Class dummy_tool_provider. * * A class that extends tool_provider so that we can expose the protected methods that we have overridden. * * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dummy_tool_provider extends tool_provider { /** * Exposes tool_provider::onError(). */ public function onError() { parent::onError(); } /** * Exposes tool_provider::onLaunch(). */ public function onLaunch() { parent::onLaunch(); } /** * Exposes tool_provider::onRegister(). */ public function onRegister() { parent::onRegister(); } /** * Expose protected variable errorOutput. * * @return string */ public function get_error_output() { return $this->errorOutput; } } /** * Class dummy_tool_consumer * * A class that extends ToolConsumer in order to override and simulate sending and receiving data to tool consumer endpoint. * * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dummy_tool_consumer extends ToolConsumer { /** * @var bool Flag to indicate whether to send an OK response or a failed response. */ protected $success = false; /** * dummy_tool_consumer constructor. * * @param null|string $key * @param mixed|null $dataconnector * @param bool $autoenable * @param bool $success */ public function __construct($key = null, $dataconnector = null, $autoenable = false, $success = false) { parent::__construct($key, $dataconnector, $autoenable); $this->success = $success; } /** * Override ToolConsumer::doServiceRequest() to simulate sending/receiving data to and from the tool consumer. * * @param object $service * @param string $method * @param string $format * @param mixed $data * @return HTTPMessage */ public function doServiceRequest($service, $method, $format, $data) { $response = (object)['tool_proxy_guid' => 1]; $header = ToolConsumer::addSignature($service->endpoint, $this->getKey(), $this->secret, $data, $method, $format); $http = new HTTPMessage($service->endpoint, $method, $data, $header); if ($this->success) { $http->responseJson = $response; $http->ok = true; $http->status = 201; } return $http; } } configure.php000064400000007737152266447110007264 0ustar00. /** * Returns the deep link resource via a POST to the platform. * * @package enrol_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\http_client; use enrol_lti\local\ltiadvantage\lib\lti_cookie; use enrol_lti\local\ltiadvantage\lib\launch_cache_session; use enrol_lti\local\ltiadvantage\lib\issuer_database; use enrol_lti\local\ltiadvantage\repository\application_registration_repository; use enrol_lti\local\ltiadvantage\repository\deployment_repository; use enrol_lti\local\ltiadvantage\repository\published_resource_repository; use Packback\Lti1p3\DeepLinkResources\Resource; use Packback\Lti1p3\LtiConstants; use Packback\Lti1p3\LtiLineitem; use Packback\Lti1p3\LtiMessageLaunch; use Packback\Lti1p3\LtiServiceConnector; require(__DIR__.'/../../config.php'); require_once(__DIR__.'/lib.php'); global $CFG, $DB, $PAGE, $USER; require_once($CFG->libdir . '/filelib.php'); require_login(null, false); require_sesskey(); $launchid = required_param('launchid', PARAM_TEXT); $modules = optional_param_array('modules', [], PARAM_INT); $grades = optional_param_array('grades', [], PARAM_INT); $sesscache = new launch_cache_session(); $issdb = new issuer_database(new application_registration_repository(), new deployment_repository()); $cookie = new lti_cookie(); $serviceconnector = new LtiServiceConnector($sesscache, new http_client()); $messagelaunch = LtiMessageLaunch::fromCache($launchid, $issdb, $sesscache, $cookie, $serviceconnector); if (!$messagelaunch->isDeepLinkLaunch()) { throw new coding_exception('Configuration can only be accessed as part of a content item selection deep link '. 'launch.'); } $sesscache->purge(); // Get the selected resources and create the resource link content items to post back. $resourcerepo = new published_resource_repository(); $resources = $resourcerepo->find_all_by_ids_for_user($modules, $USER->id); $contentitems = []; foreach ($resources as $resource) { $contentitem = Resource::new() ->setUrl($CFG->wwwroot . '/enrol/lti/launch.php') ->setCustomParams(['id' => $resource->get_uuid()]) ->setTitle($resource->get_name()); // If the activity supports grading, and the user has selected it, then include line item information. if ($resource->supports_grades() && in_array($resource->get_id(), $grades)) { require_once($CFG->libdir . '/gradelib.php'); $lineitem = LtiLineitem::new() ->setScoreMaximum($resource->get_grademax()) ->setResourceId($resource->get_uuid()); $contentitem->setLineitem($lineitem); } $contentitems[] = $contentitem; } global $USER, $CFG, $OUTPUT; $PAGE->set_context(context_system::instance()); $url = new moodle_url('/enrol/lti/configure.php'); $PAGE->set_url($url); $PAGE->set_pagelayout('popup'); echo $OUTPUT->header(); $dl = $messagelaunch->getDeepLink(); $formactionurl = $messagelaunch->getLaunchData()[LtiConstants::DL_DEEP_LINK_SETTINGS]['deep_link_return_url']; echo << HTML; tool.php000064400000007231152266447110006245 0ustar00. /** * The main entry point for the external system. * * @package enrol_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); $toolid = required_param('id', PARAM_INT); $PAGE->set_context(context_system::instance()); $url = new moodle_url('/enrol/lti/tool.php'); $PAGE->set_url($url); $PAGE->set_pagelayout('popup'); $PAGE->set_title(get_string('opentool', 'enrol_lti')); // Get the tool. $tool = \enrol_lti\helper::get_lti_tool($toolid); // Check if the authentication plugin is disabled. if (!is_enabled_auth('lti')) { throw new \moodle_exception('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti')); exit(); } // Check if the enrolment plugin is disabled. if (!enrol_is_enabled('lti')) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); exit(); } // Check if the enrolment instance is disabled. if ($tool->status != ENROL_INSTANCE_ENABLED) { throw new \moodle_exception('enrolisdisabled', 'enrol_lti'); exit(); } // Check if the enrolment instance has been upgraded to a newer LTI version. if ($tool->ltiversion != 'LTI-1p0/LTI-2p0') { throw new \moodle_exception('enrolltiversionincorrect', 'enrol_lti'); exit(); } $consumerkey = required_param('oauth_consumer_key', PARAM_TEXT); $ltiversion = optional_param('lti_version', null, PARAM_TEXT); $messagetype = required_param('lti_message_type', PARAM_TEXT); // Only accept launch requests from this endpoint. if ($messagetype != "basic-lti-launch-request") { throw new \moodle_exception('invalidrequest', 'enrol_lti'); exit(); } // Initialise tool provider. $toolprovider = new \enrol_lti\tool_provider($toolid); // Special handling for LTIv1 launch requests. if ($ltiversion === \IMSGlobal\LTI\ToolProvider\ToolProvider::LTI_VERSION1) { $dataconnector = new \enrol_lti\data_connector(); $consumer = new \IMSGlobal\LTI\ToolProvider\ToolConsumer($consumerkey, $dataconnector); // Check if the consumer has already been registered to the enrol_lti_lti2_consumer table. Register if necessary. $consumer->ltiVersion = \IMSGlobal\LTI\ToolProvider\ToolProvider::LTI_VERSION1; // For LTIv1, set the tool secret as the consumer secret. $consumer->secret = $tool->secret; $consumer->name = optional_param('tool_consumer_instance_name', '', PARAM_TEXT); $consumer->consumerName = $consumer->name; $consumer->consumerGuid = optional_param('tool_consumer_instance_guid', null, PARAM_TEXT); $consumer->consumerVersion = optional_param('tool_consumer_info_version', null, PARAM_TEXT); $consumer->enabled = true; $consumer->protected = true; $consumer->save(); // Set consumer to tool provider. $toolprovider->consumer = $consumer; // Map tool consumer and published tool, if necessary. $toolprovider->map_tool_to_consumer(); } // Handle the request. $toolprovider->handleRequest(); echo $OUTPUT->header(); echo $OUTPUT->footer(); ims-blti/blti.php000064400000024736152266447110007753 0ustar00dirroot . '/enrol/lti/ims-blti/OAuth.php'); require_once($CFG->dirroot . '/enrol/lti/ims-blti/TrivialOAuthDataStore.php'); // Returns true if this is a Basic LTI message // with minimum values to meet the protocol function is_basic_lti_request() { $good_message_type = $_REQUEST["lti_message_type"] == "basic-lti-launch-request"; $good_lti_version = ($_REQUEST["lti_version"] == "LTI-1p0" or $_REQUEST["lti_version"] == "LTI-1.0"); $good_lti_version = $good_lti_version || ($_REQUEST["lti_version"] == "LTI-2p0" or $_REQUEST["lti_version"] == "LTI-2.0"); $resource_link_id = $_REQUEST["resource_link_id"]; if ($good_message_type and $good_lti_version and isset($resource_link_id) ) return(true); return false; } // Basic LTI Class that does the setup and provides utility // functions class BLTI { public $valid = false; public $complete = false; public $message = false; public $basestring = false; public $info = false; public $row = false; public $context_id = false; // Override context_id function __construct($parm=false, $usesession=true, $doredirect=true) { // If this request is not an LTI Launch, either // give up or try to retrieve the context from session if ( ! is_basic_lti_request() ) { if ( $usesession === false ) return; if ( strlen(session_id()) > 0 ) { $row = $_SESSION['_basiclti_lti_row']; if ( isset($row) ) $this->row = $row; $context_id = $_SESSION['_basiclti_lti_context_id']; if ( isset($context_id) ) $this->context_id = $context_id; $info = $_SESSION['_basic_lti_context']; if ( isset($info) ) { $this->info = $info; $this->valid = true; return; } $this->message = "Could not find context in session"; return; } $this->message = "Session not available"; return; } // Insure we have a valid launch if ( empty($_REQUEST["oauth_consumer_key"]) ) { $this->message = "Missing oauth_consumer_key in request"; return; } $oauth_consumer_key = $_REQUEST["oauth_consumer_key"]; // Find the secret - either form the parameter as a string or // look it up in a database from parameters we are given $secret = false; $row = false; if ( is_string($parm) ) { $secret = $parm; } else if ( ! is_array($parm) ) { $this->message = "Constructor requires a secret or database information."; return; } // Verify the message signature $store = new TrivialOAuthDataStore(); $store->add_consumer($oauth_consumer_key, $secret); $server = new OAuthServer($store); $method = new OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = OAuthRequest::from_request(); $this->basestring = $request->get_signature_base_string(); try { $server->verify_request($request); $this->valid = true; } catch (Exception $e) { $this->message = $e->getMessage(); return; } // Store the launch information in the session for later $newinfo = array(); foreach($_POST as $key => $value ) { if ( $key == "basiclti_submit" ) continue; if ( strpos($key, "oauth_") === false ) { $newinfo[$key] = $value; continue; } if ( $key == "oauth_consumer_key" ) { $newinfo[$key] = $value; continue; } } //Added abertranb to decode base 64 20120801 if (isset($newinfo['custom_lti_message_encoded_base64']) && $newinfo['custom_lti_message_encoded_base64']==1){ $newinfo = $this->decodeBase64($newinfo); } $this->info = $newinfo; if ( $usesession == true and strlen(session_id()) > 0 ) { $_SESSION['_basic_lti_context'] = $this->info; unset($_SESSION['_basiclti_lti_row']); unset($_SESSION['_basiclti_lti_context_id']); if ( $this->row ) $_SESSION['_basiclti_lti_row'] = $this->row; if ( $this->context_id ) $_SESSION['_basiclti_lti_context_id'] = $this->context_id; } if ( $this->valid && $doredirect ) { $this->redirect(); $this->complete = true; } } function addSession($location) { if ( ini_get('session.use_cookies') == 0 ) { if ( strpos($location,'?') > 0 ) { $location = $location . '&'; } else { $location = $location . '?'; } $location = $location . session_name() . '=' . session_id(); } return $location; } function isInstructor() { $roles = $this->info['roles']; $roles = strtolower($roles); if ( ! ( strpos($roles,"instructor") === false ) ) return true; if ( ! ( strpos($roles,"administrator") === false ) ) return true; return false; } function getUserEmail() { # set default email in the event privacy settings don't pass in email. $email = $this->info['user_id'] . "@ltiuser.com"; if ( isset($this->info['lis_person_contact_email_primary']) ) $email = $this->info['lis_person_contact_email_primary']; # Sakai Hack if ( isset($this->info['lis_person_contact_emailprimary']) ) $email = $this->info['lis_person_contact_emailprimary']; return $email; } function getUserShortName() { $email = $this->getUserEmail(); $givenname = $this->info['lis_person_name_given']; $familyname = $this->info['lis_person_name_family']; $fullname = $this->info['lis_person_name_full']; if ( strlen($email) > 0 ) return $email; if ( strlen($givenname) > 0 ) return $givenname; if ( strlen($familyname) > 0 ) return $familyname; return $this->getUserName(); } function getUserName() { $givenname = $this->info['lis_person_name_given']; $familyname = $this->info['lis_person_name_family']; $fullname = $this->info['lis_person_name_full']; if ( strlen($fullname) > 0 ) return $fullname; if ( strlen($familyname) > 0 and strlen($givenname) > 0 ) return $givenname + $familyname; if ( strlen($givenname) > 0 ) return $givenname; if ( strlen($familyname) > 0 ) return $familyname; return $this->getUserEmail(); } function getUserKey() { $oauth = $this->info['oauth_consumer_key']; $id = $this->info['user_id']; if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; return false; } function getUserImage() { $image = $this->info['user_image']; if ( strlen($image) > 0 ) return $image; $email = $this->getUserEmail(); if ( $email === false ) return false; $size = 40; $grav_url = $_SERVER['HTTPS'] ? 'https://' : 'http://'; $grav_url = $grav_url . "www.gravatar.com/avatar.php?gravatar_id=".md5( strtolower($email) )."&size=".$size; return $grav_url; } function getResourceKey() { $oauth = $this->info['oauth_consumer_key']; $id = $this->info['resource_link_id']; if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; return false; } function getResourceTitle() { $title = $this->info['resource_link_title']; if ( strlen($title) > 0 ) return $title; return false; } function getConsumerKey() { $oauth = $this->info['oauth_consumer_key']; return $oauth; } function getCourseKey() { if ( $this->context_id ) return $this->context_id; $oauth = $this->info['oauth_consumer_key']; $id = $this->info['context_id']; if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id; return false; } function getCourseName() { $label = $this->info['context_label']; $title = $this->info['context_title']; $id = $this->info['context_id']; if ( strlen($label) > 0 ) return $label; if ( strlen($title) > 0 ) return $title; if ( strlen($id) > 0 ) return $id; return false; } // TODO: Add javasript version if headers are already sent function redirect() { $host = $_SERVER['HTTP_HOST']; $uri = $_SERVER['PHP_SELF']; $location = $_SERVER['HTTPS'] ? 'https://' : 'http://'; $location = $location . $host . $uri; $location = $this->addSession($location); header("Location: $location"); } function dump() { if ( ! $this->valid or $this->info == false ) return "Context not valid\n"; $ret = ""; if ( $this->isInstructor() ) { $ret .= "isInstructor() = true\n"; } else { $ret .= "isInstructor() = false\n"; } $ret .= "getUserKey() = ".$this->getUserKey()."\n"; $ret .= "getUserEmail() = ".$this->getUserEmail()."\n"; $ret .= "getUserShortName() = ".$this->getUserShortName()."\n"; $ret .= "getUserName() = ".$this->getUserName()."\n"; $ret .= "getUserImage() = ".$this->getUserImage()."\n"; $ret .= "getResourceKey() = ".$this->getResourceKey()."\n"; $ret .= "getResourceTitle() = ".$this->getResourceTitle()."\n"; $ret .= "getCourseName() = ".$this->getCourseName()."\n"; $ret .= "getCourseKey() = ".$this->getCourseKey()."\n"; $ret .= "getConsumerKey() = ".$this->getConsumerKey()."\n"; return $ret; } /** * Data submitter are in base64 then we have to decode * @author Antoni Bertran (antoni@tresipunt.com) * @param $info array * @date 20120801 */ function decodeBase64($info) { $keysNoEncode = array("lti_version", "lti_message_type", "tool_consumer_instance_description", "tool_consumer_instance_guid", "oauth_consumer_key", "custom_lti_message_encoded_base64", "oauth_nonce", "oauth_version", "oauth_callback", "oauth_timestamp", "basiclti_submit", "oauth_signature_method", "ext_ims_lis_memberships_id", "ext_ims_lis_memberships_url"); foreach ($info as $key => $item){ if (!in_array($key, $keysNoEncode)) $info[$key] = base64_decode($item); } return $info; } } ?> ims-blti/TrivialOAuthDataStore.php000064400000002571152266447110013174 0ustar00dirroot . '/enrol/lti/ims-blti/OAuth.php'); /** * A Trivial memory-based store - no support for tokens */ class TrivialOAuthDataStore extends OAuthDataStore { private $consumers = array(); function add_consumer($consumer_key, $consumer_secret) { $this->consumers[$consumer_key] = $consumer_secret; } function lookup_consumer($consumer_key) { if ( strpos($consumer_key, "http://" ) === 0 ) { $consumer = new OAuthConsumer($consumer_key,"secret", NULL); return $consumer; } if ( $this->consumers[$consumer_key] ) { $consumer = new OAuthConsumer($consumer_key,$this->consumers[$consumer_key], NULL); return $consumer; } return NULL; } function lookup_token($consumer, $token_type, $token) { return new OAuthToken($consumer, ""); } // Return NULL if the nonce has not been used // Return $nonce if the nonce was previously used function lookup_nonce($consumer, $token, $nonce, $timestamp) { // Should add some clever logic to keep nonces from // being reused - for no we are really trusting // that the timestamp will save us return NULL; } function new_request_token($consumer) { return NULL; } function new_access_token($token, $consumer) { return NULL; } } ?>ims-blti/OAuth.php000064400000055340152266447110010034 0ustar00key = $key; $this->secret = $secret; $this->callback_url = $callback_url; } function __toString() { return "OAuthConsumer[key=$this->key,secret=$this->secret]"; } } class OAuthToken { // access tokens and request tokens public $key; public $secret; /** * key = the token * secret = the token secret */ function __construct($key, $secret) { $this->key = $key; $this->secret = $secret; } /** * generates the basic string serialization of a token that a server * would respond to request_token and access_token calls with */ function to_string() { return "oauth_token=" . OAuthUtil::urlencode_rfc3986($this->key) . "&oauth_token_secret=" . OAuthUtil::urlencode_rfc3986($this->secret); } function __toString() { return $this->to_string(); } } class OAuthSignatureMethod { public function check_signature(&$request, $consumer, $token, $signature) { $built = $this->build_signature($request, $consumer, $token); return $built == $signature; } } class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { function get_name() { return "HMAC-SHA1"; } public function build_signature($request, $consumer, $token) { global $OAuth_last_computed_signature; $OAuth_last_computed_signature = false; $base_string = $request->get_signature_base_string(); $request->base_string = $base_string; $key_parts = array( $consumer->secret, ($token) ? $token->secret : "" ); $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); $key = implode('&', $key_parts); $computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true)); $OAuth_last_computed_signature = $computed_signature; return $computed_signature; } } class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { public function get_name() { return "PLAINTEXT"; } public function build_signature($request, $consumer, $token) { $sig = array( OAuthUtil::urlencode_rfc3986($consumer->secret) ); if ($token) { array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret)); } else { array_push($sig, ''); } $raw = implode("&", $sig); // for debug purposes $request->base_string = $raw; return OAuthUtil::urlencode_rfc3986($raw); } } class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { public function get_name() { return "RSA-SHA1"; } protected function fetch_public_cert(&$request) { // not implemented yet, ideas are: // (1) do a lookup in a table of trusted certs keyed off of consumer // (2) fetch via http using a url provided by the requester // (3) some sort of specific discovery code based on request // // either way should return a string representation of the certificate throw Exception("fetch_public_cert not implemented"); } protected function fetch_private_cert(&$request) { // not implemented yet, ideas are: // (1) do a lookup in a table of trusted certs keyed off of consumer // // either way should return a string representation of the certificate throw Exception("fetch_private_cert not implemented"); } public function build_signature(&$request, $consumer, $token) { $base_string = $request->get_signature_base_string(); $request->base_string = $base_string; // Fetch the private key cert based on the request $cert = $this->fetch_private_cert($request); // Pull the private key ID from the certificate $privatekeyid = openssl_get_privatekey($cert); // Sign using the key $ok = openssl_sign($base_string, $signature, $privatekeyid); // Avoid passing null values to base64_encode. if (!$ok) { throw new OAuthException("OpenSSL unable to sign data"); } return base64_encode($signature); } public function check_signature(&$request, $consumer, $token, $signature) { $decoded_sig = base64_decode($signature); $base_string = $request->get_signature_base_string(); // Fetch the public key cert based on the request $cert = $this->fetch_public_cert($request); // Pull the public key ID from the certificate $publickeyid = openssl_get_publickey($cert); // Check the computed signature against the one passed in the query $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); return $ok == 1; } } class OAuthRequest { private $parameters; private $http_method; private $http_url; // for debug purposes public $base_string; public static $version = '1.0'; public static $POST_INPUT = 'php://input'; function __construct($http_method, $http_url, $parameters=NULL) { @$parameters or $parameters = array(); $this->parameters = $parameters; $this->http_method = $http_method; $this->http_url = $http_url; } /** * attempt to build up a request from what was passed to the server */ public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https'; $port = ""; if ( $_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" && strpos(':', $_SERVER['HTTP_HOST']) < 0 ) { $port = ':' . $_SERVER['SERVER_PORT'] ; } @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $port . $_SERVER['REQUEST_URI']; @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; // We weren't handed any parameters, so let's find the ones relevant to // this request. // If you run XML-RPC or similar you should use this to provide your own // parsed parameter-list if (!$parameters) { // Find request headers $request_headers = OAuthUtil::get_headers(); // Parse the query-string to find GET parameters $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); $ourpost = $_POST; // Add POST Parameters if they exist $parameters = array_merge($parameters, $ourpost); // We have a Authorization-header with OAuth data. Parse the header // and add those overriding any duplicates from GET or POST if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { $header_parameters = OAuthUtil::split_header( $request_headers['Authorization'] ); $parameters = array_merge($parameters, $header_parameters); } } return new OAuthRequest($http_method, $http_url, $parameters); } /** * pretty much a helper function to set up the request */ public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { @$parameters or $parameters = array(); $defaults = array("oauth_version" => OAuthRequest::$version, "oauth_nonce" => OAuthRequest::generate_nonce(), "oauth_timestamp" => OAuthRequest::generate_timestamp(), "oauth_consumer_key" => $consumer->key); if ($token) $defaults['oauth_token'] = $token->key; $parameters = array_merge($defaults, $parameters); // Parse the query-string to find and add GET parameters $parts = parse_url($http_url); if ( !empty($parts['query']) ) { $qparms = OAuthUtil::parse_parameters($parts['query']); $parameters = array_merge($qparms, $parameters); } return new OAuthRequest($http_method, $http_url, $parameters); } public function set_parameter($name, $value, $allow_duplicates = true) { if ($allow_duplicates && isset($this->parameters[$name])) { // We have already added parameter(s) with this name, so add to the list if (is_scalar($this->parameters[$name])) { // This is the first duplicate, so transform scalar (string) // into an array so we can add the duplicates $this->parameters[$name] = array($this->parameters[$name]); } $this->parameters[$name][] = $value; } else { $this->parameters[$name] = $value; } } public function get_parameter($name) { return isset($this->parameters[$name]) ? $this->parameters[$name] : null; } public function get_parameters() { return $this->parameters; } public function unset_parameter($name) { unset($this->parameters[$name]); } /** * The request parameters, sorted and concatenated into a normalized string. * @return string */ public function get_signable_parameters() { // Grab all parameters $params = $this->parameters; // Remove oauth_signature if present // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") if (isset($params['oauth_signature'])) { unset($params['oauth_signature']); } return OAuthUtil::build_http_query($params); } /** * Returns the base string of this request * * The base string defined as the method, the url * and the parameters (normalized), each urlencoded * and the concated with &. */ public function get_signature_base_string() { $parts = array( $this->get_normalized_http_method(), $this->get_normalized_http_url(), $this->get_signable_parameters() ); $parts = OAuthUtil::urlencode_rfc3986($parts); return implode('&', $parts); } /** * just uppercases the http method */ public function get_normalized_http_method() { return strtoupper($this->http_method); } /** * parses the url and rebuilds it to be * scheme://host/path */ public function get_normalized_http_url() { $parts = parse_url($this->http_url); $port = @$parts['port']; $scheme = $parts['scheme']; $host = $parts['host']; $path = @$parts['path']; $port or $port = ($scheme == 'https') ? '443' : '80'; if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { $host = "$host:$port"; } return "$scheme://$host$path"; } /** * builds a url usable for a GET request */ public function to_url() { $post_data = $this->to_postdata(); $out = $this->get_normalized_http_url(); if ($post_data) { $out .= '?'.$post_data; } return $out; } /** * builds the data one would send in a POST request */ public function to_postdata() { return OAuthUtil::build_http_query($this->parameters); } /** * builds the Authorization: header */ public function to_header() { $out ='Authorization: OAuth realm=""'; $total = array(); foreach ($this->parameters as $k => $v) { if (substr($k, 0, 5) != "oauth") continue; if (is_array($v)) { throw new OAuthException('Arrays not supported in headers'); } $out .= ',' . OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"'; } return $out; } public function __toString() { return $this->to_url(); } public function sign_request($signature_method, $consumer, $token) { $this->set_parameter( "oauth_signature_method", $signature_method->get_name(), false ); $signature = $this->build_signature($signature_method, $consumer, $token); $this->set_parameter("oauth_signature", $signature, false); } public function build_signature($signature_method, $consumer, $token) { $signature = $signature_method->build_signature($this, $consumer, $token); return $signature; } /** * util function: current timestamp */ private static function generate_timestamp() { return time(); } /** * util function: current nonce */ private static function generate_nonce() { $mt = microtime(); $rand = mt_rand(); return md5($mt . $rand); // md5s look nicer than numbers } } class OAuthServer { protected $timestamp_threshold = 300; // in seconds, five minutes protected $version = 1.0; // hi blaine protected $signature_methods = array(); protected $data_store; function __construct($data_store) { $this->data_store = $data_store; } public function add_signature_method($signature_method) { $this->signature_methods[$signature_method->get_name()] = $signature_method; } // high level functions /** * process a request_token request * returns the request token on success */ public function fetch_request_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); // no token required for the initial token request $token = NULL; $this->check_signature($request, $consumer, $token); $new_token = $this->data_store->new_request_token($consumer); return $new_token; } /** * process an access_token request * returns the access token on success */ public function fetch_access_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); // requires authorized request token $token = $this->get_token($request, $consumer, "request"); $this->check_signature($request, $consumer, $token); $new_token = $this->data_store->new_access_token($token, $consumer); return $new_token; } /** * verify an api call, checks all the parameters */ public function verify_request(&$request) { global $OAuth_last_computed_signature; $OAuth_last_computed_signature = false; $this->get_version($request); $consumer = $this->get_consumer($request); $token = $this->get_token($request, $consumer, "access"); $this->check_signature($request, $consumer, $token); return array($consumer, $token); } // Internals from here /** * version 1 */ private function get_version(&$request) { $version = $request->get_parameter("oauth_version"); if (!$version) { $version = 1.0; } if ($version && $version != $this->version) { throw new OAuthException("OAuth version '$version' not supported"); } return $version; } /** * figure out the signature with some defaults */ private function get_signature_method(&$request) { $signature_method = @$request->get_parameter("oauth_signature_method"); if (!$signature_method) { $signature_method = "PLAINTEXT"; } if (!in_array($signature_method, array_keys($this->signature_methods))) { throw new OAuthException( "Signature method '$signature_method' not supported " . "try one of the following: " . implode(", ", array_keys($this->signature_methods)) ); } return $this->signature_methods[$signature_method]; } /** * try to find the consumer for the provided request's consumer key */ private function get_consumer(&$request) { $consumer_key = @$request->get_parameter("oauth_consumer_key"); if (!$consumer_key) { throw new OAuthException("Invalid consumer key"); } $consumer = $this->data_store->lookup_consumer($consumer_key); if (!$consumer) { throw new OAuthException("Invalid consumer"); } return $consumer; } /** * try to find the token for the provided request's token key */ private function get_token(&$request, $consumer, $token_type="access") { $token_field = @$request->get_parameter('oauth_token'); if ( !$token_field) return false; $token = $this->data_store->lookup_token( $consumer, $token_type, $token_field ); if (!$token) { throw new OAuthException("Invalid $token_type token: $token_field"); } return $token; } /** * all-in-one function to check the signature on a request * should guess the signature method appropriately */ private function check_signature(&$request, $consumer, $token) { // this should probably be in a different method global $OAuth_last_computed_signature; $OAuth_last_computed_signature = false; $timestamp = @$request->get_parameter('oauth_timestamp'); $nonce = @$request->get_parameter('oauth_nonce'); $this->check_timestamp($timestamp); $this->check_nonce($consumer, $token, $nonce, $timestamp); $signature_method = $this->get_signature_method($request); $signature = $request->get_parameter('oauth_signature'); $valid_sig = $signature_method->check_signature( $request, $consumer, $token, $signature ); if (!$valid_sig) { $ex_text = "Invalid signature"; if ( $OAuth_last_computed_signature ) { $ex_text = $ex_text . " ours= $OAuth_last_computed_signature yours=$signature"; } throw new OAuthException($ex_text); } } /** * check that the timestamp is new enough */ private function check_timestamp($timestamp) { // verify that timestamp is recentish $now = time(); if ($now - $timestamp > $this->timestamp_threshold) { throw new OAuthException( "Expired timestamp, yours $timestamp, ours $now" ); } } /** * check that the nonce is not repeated */ private function check_nonce($consumer, $token, $nonce, $timestamp) { // verify that the nonce is uniqueish $found = $this->data_store->lookup_nonce( $consumer, $token, $nonce, $timestamp ); if ($found) { throw new OAuthException("Nonce already used: $nonce"); } } } class OAuthDataStore { function lookup_consumer($consumer_key) { // implement me } function lookup_token($consumer, $token_type, $token) { // implement me } function lookup_nonce($consumer, $token, $nonce, $timestamp) { // implement me } function new_request_token($consumer) { // return a new token attached to this consumer } function new_access_token($token, $consumer) { // return a new access token attached to this consumer // for the user associated with this token if the request token // is authorized // should also invalidate the request token } } class OAuthUtil { public static function urlencode_rfc3986($input) { if (is_array($input)) { return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); } else if (is_scalar($input)) { return str_replace( '+', ' ', str_replace('%7E', '~', rawurlencode($input)) ); } else { return ''; } } // This decode function isn't taking into consideration the above // modifications to the encoding process. However, this method doesn't // seem to be used anywhere so leaving it as is. public static function urldecode_rfc3986($string) { return urldecode($string); } // Utility function for turning the Authorization: header into // parameters, has to do some unescaping // Can filter out any non-oauth parameters if needed (default behaviour) public static function split_header($header, $only_allow_oauth_parameters = true) { $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; $offset = 0; $params = array(); while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { $match = $matches[0]; $header_name = $matches[2][0]; $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); } $offset = $match[1] + strlen($match[0]); } if (isset($params['realm'])) { unset($params['realm']); } return $params; } // helper to try to sort out headers for people who aren't running apache public static function get_headers() { if (function_exists('apache_request_headers')) { // we need this to get the actual Authorization: header // because apache tends to tell us it doesn't exist return apache_request_headers(); } // otherwise we don't have apache and are just going to have to hope // that $_SERVER actually contains what we need $out = array(); foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) == "HTTP_") { // this is chaos, basically it is just there to capitalize the first // letter of every word that is not an initial HTTP and strip HTTP // code from przemek $key = str_replace( " ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) ); $out[$key] = $value; } } return $out; } // This function takes a input like a=b&a=c&d=e and returns the parsed // parameters like this // array('a' => array('b','c'), 'd' => 'e') public static function parse_parameters( $input ) { if (!isset($input) || !$input) return array(); $pairs = explode('&', $input); $parsed_parameters = array(); foreach ($pairs as $pair) { $split = explode('=', $pair, 2); $parameter = OAuthUtil::urldecode_rfc3986($split[0]); $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; if (isset($parsed_parameters[$parameter])) { // We have already recieved parameter(s) with this name, so add to the list // of parameters with this name if (is_scalar($parsed_parameters[$parameter])) { // This is the first duplicate, so transform scalar (string) into an array // so we can add the duplicates $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); } $parsed_parameters[$parameter][] = $value; } else { $parsed_parameters[$parameter] = $value; } } return $parsed_parameters; } public static function build_http_query($params) { if (!$params) return ''; // Urlencode both keys and values $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); $values = OAuthUtil::urlencode_rfc3986(array_values($params)); $params = array_combine($keys, $values); // Parameters are sorted by name, using lexicographical byte value ordering. // Ref: Spec: 9.1.1 (1) uksort($params, 'strcmp'); $pairs = array(); foreach ($params as $parameter => $value) { if (is_array($value)) { // If two or more parameters share the same name, they are sorted by their value // Ref: Spec: 9.1.1 (1) natsort($value); foreach ($value as $duplicate_value) { $pairs[] = $parameter . '=' . $duplicate_value; } } else { $pairs[] = $parameter . '=' . $value; } } // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) // Each name-value pair is separated by an '&' character (ASCII code 38) return implode('&', $pairs); } } ?> ims-blti/readme_moodle.txt000064400000001735152266447110011637 0ustar00This library was originally published by the IMS at https://code.google.com/p/ims-dev/ which no longer exists. The current code was taken from https://github.com/jfederico/ims-dev/tree/master/basiclti/php-simple/ims-blti - with several changes to the code (including bug fixes). As the library is no longer supported upgrades are not possible. In future releases we should look into using a supported library. 2022-01-05 - MDL-73502 - Removed get_magic_quotes_gpc() use, was returning false since ages ago. 2022-01-20 - MDL-73523 - Conditional openssl_free_key() use, deprecated by PHP 8.0 2022-03-05 - MDL-73520 - replace deprecated php_errormsg with error_get_last(), deprecated by PHP 8.0 2023-05-03 - MDL-77840 - Throw exception on openssl_sign to avoid null reaching base64_encode, deprecated by PHP 8.1 2023-05-17 - MDL-77350 - Added $callback_url property to class OAuthConsumer to comply with PHP 8.2 2024-03-21 - MDL-73700 - Removing old PHP version check (related to MDL-73523) ims-blti/blti_util.php000064400000020541152266447110010776 0ustar00 "120988f929-274612", "resource_link_title" => "Weekly Blog", "resource_link_description" => "Each student needs to reflect on the weekly reading. These should be one paragraph long.", "user_id" => "292832126", "roles" => "Instructor", // or Learner "lis_person_name_full" => 'Jane Q. Public', "lis_person_contact_email_primary" => "user@school.edu", "lis_person_sourcedid" => "school.edu:user", "context_id" => "456434513", "context_title" => "Design of Personal Environments", "context_label" => "SI182", ); return $parms; } function validateDescriptor($descriptor) { $xml = new SimpleXMLElement($xmldata); if ( ! $xml ) { echo("Error parsing Descriptor XML\n"); return; } $launch_url = $xml->secure_launch_url[0]; if ( ! $launch_url ) $launch_url = $xml->launch_url[0]; if ( $launch_url ) $launch_url = (string) $launch_url; return $launch_url; } // Parse a descriptor function launchInfo($xmldata) { $xml = new SimpleXMLElement($xmldata); if ( ! $xml ) { echo("Error parsing Descriptor XML\n"); return; } $launch_url = $xml->secure_launch_url[0]; if ( ! $launch_url ) $launch_url = $xml->launch_url[0]; if ( $launch_url ) $launch_url = (string) $launch_url; $custom = array(); if ( $xml->custom[0]->parameter ) foreach ( $xml->custom[0]->parameter as $resource) { $key = (string) $resource['key']; $key = strtolower($key); $nk = ""; for($i=0; $i < strlen($key); $i++) { $ch = substr($key,$i,1); if ( $ch >= "a" && $ch <= "z" ) $nk .= $ch; else if ( $ch >= "0" && $ch <= "9" ) $nk .= $ch; else $nk .= "_"; } $value = (string) $resource; $custom["custom_".$nk] = $value; } return array("launch_url" => $launch_url, "custom" => $custom ) ; } function signParameters($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret, $submit_text = false, $org_id = false, $org_desc = false) { global $last_base_string; $parms = $oldparms; if ( ! isset($parms["lti_version"]) ) $parms["lti_version"] = "LTI-1p0"; if ( ! isset($parms["lti_message_type"]) ) $parms["lti_message_type"] = "basic-lti-launch-request"; if ( ! isset($parms["oauth_callback"]) ) $parms["oauth_callback"] = "about:blank"; if ( $org_id ) $parms["tool_consumer_instance_guid"] = $org_id; if ( $org_desc ) $parms["tool_consumer_instance_description"] = $org_desc; if ( $submit_text ) $parms["ext_submit"] = $submit_text; $test_token = ''; $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); $acc_req->sign_request($hmac_method, $test_consumer, $test_token); // Pass this back up "out of band" for debugging $last_base_string = $acc_req->get_signature_base_string(); $newparms = $acc_req->get_parameters(); return $newparms; } function signOnly($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret) { global $last_base_string; $parms = $oldparms; $test_token = ''; $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); $acc_req->sign_request($hmac_method, $test_consumer, $test_token); // Pass this back up "out of band" for debugging $last_base_string = $acc_req->get_signature_base_string(); $newparms = $acc_req->get_parameters(); return $newparms; } function postLaunchHTML($newparms, $endpoint, $debug=false, $iframeattr=false) { global $last_base_string; $r = "
\n"; if ( $iframeattr ) { $r = "
\n" ; } else { $r = "\n" ; } $submit_text = $newparms['ext_submit']; foreach($newparms as $key => $value ) { $key = htmlspecialchars($key); $value = htmlspecialchars($value); if ( $key == "ext_submit" ) { $r .= "\n"; } if ( $debug ) { $r .= "\n"; $r .= ""; $r .= get_stringIMS("toggle_debug_data","basiclti")."\n"; $r .= "
\n"; $r .= "".get_stringIMS("basiclti_endpoint","basiclti")."
\n"; $r .= $endpoint . "
\n 
\n"; $r .= "".get_stringIMS("basiclti_parameters","basiclti")."
\n"; foreach($newparms as $key => $value ) { $key = htmlspecialchars($key); $value = htmlspecialchars($value); $r .= "$key = $value
\n"; } $r .= " 
\n"; $r .= "

".get_stringIMS("basiclti_base_string","basiclti")."
\n".$last_base_string."

\n"; $r .= "
\n"; } $r .= "
\n"; if ( $iframeattr ) { $r .= "\n"; } if ( ! $debug ) { $ext_submit = "ext_submit"; $ext_submit_text = $submit_text; $r .= " \n"; } $r .= "
\n"; return $r; } /* This is a bit of homage to Moodle's pattern of internationalisation */ function get_stringIMS($key,$bundle) { return $key; } function do_post_request($url, $data, $optional_headers = null) { $params = array('http' => array( 'method' => 'POST', 'content' => $data )); if ($optional_headers !== null) { $header = $optional_headers . "\r\n"; } // $header = $header . "Content-type: application/x-www-form-urlencoded\r\n"; $params['http']['header'] = $header; $ctx = stream_context_create($params); $fp = @fopen($url, 'rb', false, $ctx); if (!$fp) { echo @stream_get_contents($fp); $message = "(No error message provided.)"; if ($error = error_get_last()) { $message = $error["message"]; } throw new Exception("Problem with $url, $message"); } $response = @stream_get_contents($fp); if ($response === false) { $message = "(No error message provided.)"; if ($error = error_get_last()) { $message = $error["message"]; } throw new Exception("Problem reading data from $url, $message"); } return $response; } ims-blti/LICENSE.txt000064400000002060152266447110010115 0ustar00The MIT License Copyright (c) 2007 Andy Smith Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ims-blti/OAuthBody.php000064400000011506152266447110010646 0ustar00add_consumer($oauth_consumer_key, $oauth_consumer_secret); $server = new OAuthServer($store); $method = new OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = OAuthRequest::from_request(); global $LastOAuthBodyBaseString; $LastOAuthBodyBaseString = $request->get_signature_base_string(); // echo($LastOAuthBodyBaseString."\n"); try { $server->verify_request($request); } catch (Exception $e) { $message = $e->getMessage(); throw new Exception("OAuth signature failed: " . $message); } $postdata = file_get_contents('php://input'); // echo($postdata); $hash = base64_encode(sha1($postdata, TRUE)); if ( $hash != $oauth_body_hash ) { throw new Exception("OAuth oauth_body_hash mismatch"); } return $postdata; } function sendOAuthBodyPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $body) { global $CFG; require_once($CFG->dirroot . '/lib/filelib.php'); $hash = base64_encode(sha1($body, TRUE)); $parms = array('oauth_body_hash' => $hash); $test_token = ''; $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); $acc_req->sign_request($hmac_method, $test_consumer, $test_token); // Pass this back up "out of band" for debugging global $LastOAuthBodyBaseString; $LastOAuthBodyBaseString = $acc_req->get_signature_base_string(); // echo($LastOAuthBodyBaseString."\m"); $headers = array(); $headers[] = $acc_req->to_header(); $headers[] = "Content-type: " . $content_type; $curl = new curl(); $curl->setHeader($headers); $response = $curl->post($endpoint, $body); return $response; } function sendOAuthParamsPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $params) { if (is_array($params)) { $body = http_build_query($params, '', '&'); } else { $body = $params; } $hash = base64_encode(sha1($body, TRUE)); $parms = $params; $parms['oauth_body_hash'] = $hash; $test_token = ''; $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms); $acc_req->sign_request($hmac_method, $test_consumer, $test_token); // Pass this back up "out of band" for debugging global $LastOAuthBodyBaseString; $LastOAuthBodyBaseString = $acc_req->get_signature_base_string(); // echo($LastOAuthBodyBaseString."\m"); $header = $acc_req->to_header(); $header = $header . "\r\nContent-type: " . $content_type . "\r\n"; $params = array('http' => array( 'method' => 'POST', 'content' => $body, 'header' => $header )); $ctx = stream_context_create($params); $fp = @fopen($endpoint, 'rb', false, $ctx); if (!$fp) { $message = "(No error message provided.)"; if ($error = error_get_last()) { $message = $error["message"]; } throw new \Exception("Problem with $endpoint, $message"); } $response = @stream_get_contents($fp); if ($response === false) { $message = "(No error message provided.)"; if ($error = error_get_last()) { $message = $error["message"]; } throw new \Exception("Problem reading data from $endpoint, $message"); } return $response; } ?>