. /** * This file contains the library of functions and constants for the basiclti module * * @package 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 moodle\mod\lti as lti; require_once($CFG->dirroot.'/mod/lti/OAuth.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_TOOL_STATE_ANY', 0); define('LTI_TOOL_STATE_CONFIGURED', 1); define('LTI_TOOL_STATE_PENDING', 2); define('LTI_TOOL_STATE_REJECTED', 3); define('LTI_SETTING_NEVER', 0); define('LTI_SETTING_ALWAYS', 1); define('LTI_SETTING_DEFAULT', 2); /** * Prints a Basic LTI activity * * $param int $basicltiid Basic LTI activity id */ function lti_view($instance, $makeobject=false) { global $PAGE, $CFG; if(empty($instance->typeid)){ $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); if($tool){ $typeid = $tool->id; } else { $typeid = null; } } else { $typeid = $instance->typeid; } 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; } //Default the organizationid if not specified if(empty($typeconfig['organizationid'])){ $urlparts = parse_url($CFG->wwwroot); $typeconfig['organizationid'] = $urlparts['host']; } $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; $key = !empty($instance->resourcekey) ? $instance->resourcekey : $typeconfig['resourcekey']; $secret = !empty($instance->password) ? $instance->password : $typeconfig['password']; $orgid = $typeconfig['organizationid']; /* Suppress this for now - Chuck $orgdesc = $typeconfig['organizationdescr']; */ $course = $PAGE->course; $requestparams = lti_build_request($instance, $typeconfig, $course); // Make sure we let the tool know what LMS they are being called from $requestparams["ext_lms"] = "moodle-2"; // Add oauth_callback to be compliant with the 1.0A spec $requestparams["oauth_callback"] = "about:blank"; $submittext = get_string('press_to_submit', 'lti'); $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret, $submittext, $orgid /*, $orgdesc*/); $debuglaunch = ( $instance->debuglaunch == 1 ); $content = lti_post_launch_html($parms, $endpoint, $debuglaunch); echo $content; } function lti_build_sourcedid($instanceid, $userid, $servicesalt){ $data = new stdClass(); $data->instanceid = $instanceid; $data->userid = $userid; $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 object $typeconfig Basic LTI tool configuration * @param object $course Course object * * @return array $request Request details */ function lti_build_request($instance, $typeconfig, $course) { global $USER, $CFG; $context = get_context_instance(CONTEXT_COURSE, $course->id); $role = lti_get_ims_role($USER, $context); $locale = $course->lang; if ( strlen($locale) < 1 ) { $locale = $CFG->lang; } $requestparams = array( "resource_link_id" => $instance->id, "resource_link_title" => $instance->name, "resource_link_description" => $instance->intro, "user_id" => $USER->id, "roles" => $role, "context_id" => $course->id, "context_label" => $course->shortname, "context_title" => $course->fullname, "launch_presentation_locale" => $locale, ); $placementsecret = $instance->servicesalt; if ( isset($placementsecret) ) { $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret)); } if ( isset($placementsecret) && ( $typeconfig['acceptgrades'] == 1 || ( $typeconfig['acceptgrades'] == 2 && $instance->instructorchoiceacceptgrades == 1 ) ) ) { $requestparams["lis_result_sourcedid"] = $sourcedid; $requestparams["ext_ims_lis_basic_outcome_url"] = $CFG->wwwroot.'/mod/lti/service.php'; } if ( isset($placementsecret) && ( $typeconfig['allowroster'] == 1 || ( $typeconfig['allowroster'] == 2 && $instance->instructorchoiceallowroster == 1 ) ) ) { $requestparams["ext_ims_lis_memberships_id"] = $sourcedid; $requestparams["ext_ims_lis_memberships_url"] = $CFG->wwwroot.'/mod/lti/service.php'; } // Send user's name and email data if appropriate if ( $typeconfig['sendname'] == 1 || ( $typeconfig['sendname'] == 2 && $instance->instructorchoicesendname == 1 ) ) { $requestparams["lis_person_name_given"] = $USER->firstname; $requestparams["lis_person_name_family"] = $USER->lastname; $requestparams["lis_person_name_full"] = $USER->firstname." ".$USER->lastname; } if ( $typeconfig['sendemailaddr'] == 1 || ( $typeconfig['sendemailaddr'] == 2 && $instance->instructorchoicesendemailaddr == 1 ) ) { $requestparams["lis_person_contact_email_primary"] = $USER->email; } // Concatenate the custom parameters from the administrator and the instructor // Instructor parameters are only taken into consideration if the administrator // has giver permission $customstr = $typeconfig['customparameters']; $instructorcustomstr = $instance->instructorcustomparameters; $custom = array(); $instructorcustom = array(); if ($customstr) { $custom = lti_split_custom_parameters($customstr); } if (!isset($typeconfig['allowinstructorcustom']) || $typeconfig['allowinstructorcustom'] == 0) { $requestparams = array_merge($custom, $requestparams); } else { if ($instructorcustomstr) { $instructorcustom = lti_split_custom_parameters($instructorcustomstr); } foreach ($instructorcustom as $key => $val) { if (array_key_exists($key, $custom)) { // Ignore the instructor's parameter } else { $custom[$key] = $val; } } $requestparams = array_merge($custom, $requestparams); } return $requestparams; } function lti_get_tool_table($tools, $id){ global $CFG, $USER; $html = ''; $typename = get_string('typename', 'lti'); $baseurl = get_string('baseurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); if($id == 'lti_configured'){ $html .= '
'.get_string('addtype', 'lti').'
'; } if (!empty($tools)) { $html .= << HTML; foreach ($tools as $type) { $date = userdate($type->timecreated); $accept = get_string('accept', 'lti'); $update = get_string('update', 'lti'); $delete = get_string('delete', 'lti'); $accepthtml = <<{$accept} HTML; $deleteaction = 'delete'; if($type->state == LTI_TOOL_STATE_CONFIGURED){ $accepthtml = ''; } if($type->state != LTI_TOOL_STATE_REJECTED) { $deleteaction = 'reject'; $delete = get_string('reject', 'lti'); } $html .= << HTML; } $html .= '
$typename $baseurl $createdon $action
{$type->name} {$type->baseurl} {$date} {$accepthtml} {$update} {$delete}
'; } else { $html .= get_string('no_' . $id, 'lti'); } return $html; } /** * Splits the custom parameters field to the various parameters * * @param string $customstr String containing the parameters * * @return Array of custom parameters */ function lti_split_custom_parameters($customstr) { $textlib = textlib_get_instance(); $lines = preg_split("/[\n;]/", $customstr); $retval = array(); foreach ($lines as $line) { $pos = strpos($line, "="); if ( $pos === false || $pos < 1 ) { continue; } $key = trim($textlib->substr($line, 0, $pos)); $val = trim($textlib->substr($line, $pos+1)); $key = lti_map_keyname($key); $retval['custom_'.$key] = $val; } return $retval; } /** * Used for building the names of the different custom parameters * * @param string $key Parameter name * * @return string Processed name */ function lti_map_keyname($key) { $textlib = textlib_get_instance(); $newkey = ""; $key = $textlib->strtolower(trim($key)); foreach (str_split($key) as $ch) { if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) { $newkey .= $ch; } else { $newkey .= '_'; } } return $newkey; } /** * Returns the IMS user role in a given context * * This function queries Moodle for an user role and * returns the correspondant IMS role * * @param StdClass $user Moodle user instance * @param StdClass $context Moodle context * * @return string IMS Role * */ function lti_get_ims_role($user, $context) { $roles = get_user_roles($context, $user->id); $rolesname = array(); foreach ($roles as $role) { $rolesname[] = $role->shortname; } if (in_array('admin', $rolesname) || in_array('coursecreator', $rolesname)) { return get_string('imsroleadmin', 'lti'); } if (in_array('editingteacher', $rolesname) || in_array('teacher', $rolesname)) { return get_string('imsroleinstructor', 'lti'); } return get_string('imsrolelearner', 'lti'); } /** * 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; $typeconfig = array(); $configs = $DB->get_records('lti_types_config', array('typeid' => $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; $filters = array('tooldomain' => $domain); $statefilter = ''; $coursefilter = ''; if($state){ $statefilter = 'AND state = :state'; } if($courseid && $courseid != $SITE->id){ $coursefilter = 'OR course = :courseid'; } $query = <<get_records_sql($query, array( 'courseid' => $courseid, 'siteid' => $SITE->id, 'tooldomain' => $domain, 'state' => $state )); } /** * Returns all basicLTI tools configured by the administrator * */ function lti_filter_get_types($course) { global $DB; if(!empty($course)){ $filter = array('course' => $course); } else { $filter = array(); } return $DB->get_records('lti_types', $filter); } function lti_get_types_for_add_instance(){ global $DB, $SITE, $COURSE; $query = <<get_records_sql($query, array('siteid' => $SITE->id, 'courseid' => $COURSE->id, 'active' => LTI_TOOL_STATE_CONFIGURED)); $types = array(); $types[0] = (object)array('name' => get_string('automatic', 'lti'), 'course' => $SITE->id); foreach($admintypes as $type) { $types[$type->id] = $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){ $urlparts = parse_url(strtolower($url)); if(!isset($urlparts['path'])){ $urlparts['path'] = ''; } if(substr($urlparts['host'], 0, 3) === 'www'){ $urllparts['host'] = substr(3); } return $urllower = $urlparts['host'] . '/' . $urlparts['path']; } 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 25 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; } /** * Prints the various configured tool types * */ function lti_filter_print_types() { global $CFG; $types = lti_filter_get_types(); if (!empty($types)) { echo ''; } else { echo '
'; echo get_string('notypes', 'lti'); echo '
'; } } /** * 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)); } function lti_set_state_for_type($id, $state){ global $DB; $DB->update_record('lti_types', 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(); $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 Instance 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 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->lti_typename = $basicltitype->name; $type->typeid = $basicltitype->id; $type->lti_toolurl = $basicltitype->baseurl; if (isset($config['resourcekey'])) { $type->lti_resourcekey = $config['resourcekey']; } if (isset($config['password'])) { $type->lti_password = $config['password']; } 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['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['debuglaunch'])) { $type->lti_debuglaunch = $config['debuglaunch']; } if (isset($config['module_class_type'])) { $type->lti_module_class_type = $config['module_class_type']; } return $type; } function lti_prepare_type_for_save($type, $config){ $type->baseurl = $config->lti_toolurl; $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl); $type->name = $config->lti_typename; $type->coursevisible = !empty($config->lti_coursevisible) ? $config->lti_coursevisible : 0; $config->lti_coursevisible = $type->coursevisible; $type->timemodified = time(); unset ($config->lti_typename); unset ($config->lti_toolurl); } function lti_update_type($type, $config){ global $DB; lti_prepare_type_for_save($type, $config); 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); } } } } 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->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 (substr($key, 0, 4)=='lti_' && !is_null($value)) { $record = new StdClass(); $record->typeid = $id; $record->name = substr($key, 4); $record->value = $value; lti_add_config($record); } } } return $id; } /** * Add a tool configuration in the database * * @param $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 $config Tool configuration * * @return Record id number */ function lti_update_config($config) { global $DB; $return = true; $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; } /** * Signs the petition to launch the external tool using OAuth * * @param $oldparms Parameters to be passed for signing * @param $endpoint url of the external tool * @param $method Method for sending the parameters (e.g. POST) * @param $oauth_consumoer_key Key * @param $oauth_consumoer_secret Secret * @param $submittext The text for the submit button * @param $orgid LMS name * @param $orgdesc LMS key */ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret, $submittext, $orgid /*, $orgdesc*/) { global $lastbasestring; $parms = $oldparms; $parms["lti_version"] = "LTI-1p0"; $parms["lti_message_type"] = "basic-lti-launch-request"; if ( $orgid ) { $parms["tool_consumer_instance_guid"] = $orgid; } /* Suppress this for now - Chuck if ( $orgdesc ) $parms["tool_consumer_instance_description"] = $orgdesc; */ $parms["ext_submit"] = $submittext; $testtoken = ''; $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); // Pass this back up "out of band" for debugging $lastbasestring = $accreq->get_signature_base_string(); $newparms = $accreq->get_parameters(); return $newparms; } /** * Posts the launch petition HTML * * @param $newparms Signed parameters * @param $endpoint URL of the external tool * @param $debug Debug (true/false) */ function lti_post_launch_html($newparms, $endpoint, $debug=false) { global $lastbasestring; $r = "
\n"; $submittext = $newparms['ext_submit']; // Contruct html for the launch parameters foreach ($newparms as $key => $value) { $key = htmlspecialchars($key); $value = htmlspecialchars($value); 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); $value = htmlspecialchars($value); $r .= "$key = $value
\n"; } $r .= " 
\n"; $r .= "

".get_string("basiclti_base_string", "lti")."
\n".$lastbasestring."

\n"; $r .= "
\n"; } $r .= "
\n"; if ( ! $debug ) { $ext_submit = "ext_submit"; $ext_submit_text = $submittext; $r .= " \n"; } return $r; } /** * Returns a link with info about the state of the basiclti submissions * * This is used by view_header to put this link at the top right of the page. * For teachers it gives the number of submitted assignments with a link * For students it gives the time of their submission. * This will be suitable for most assignment types. * * @global object * @global object * @param bool $allgroup print all groups info if user can access all groups, suitable for index.php * @return string */ function lti_submittedlink($cm, $allgroups=false) { global $CFG; $submitted = ''; $urlbase = "{$CFG->wwwroot}/mod/lti/"; $context = get_context_instance(CONTEXT_MODULE, $cm->id); if (has_capability('mod/lti:grade', $context)) { if ($allgroups and has_capability('moodle/site:accessallgroups', $context)) { $group = 0; } else { $group = groups_get_activity_group($cm); } $submitted = ''. get_string('viewsubmissions', 'lti').''; } else { if (isloggedin()) { // TODO Insert code for students if needed } } return $submitted; } function lti_get_type($typeid){ global $DB; return $DB->get_record('lti_types', array('id' => $typeid)); }