diff --git a/mod/lti/OAuth.php b/mod/lti/OAuth.php index 65e9b0f138a..e0365407e0d 100644 --- a/mod/lti/OAuth.php +++ b/mod/lti/OAuth.php @@ -181,7 +181,7 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { // (3) some sort of specific discovery code based on request // // either way should return a string representation of the certificate - throw OAuthException("fetch_public_cert not implemented"); + throw new OAuthException("fetch_public_cert not implemented"); } protected function fetch_private_cert(&$request) { @@ -189,7 +189,7 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { // (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 OAuthException("fetch_private_cert not implemented"); + throw new OAuthException("fetch_private_cert not implemented"); } public function build_signature(&$request, $consumer, $token) { diff --git a/mod/lti/OAuthBody.php b/mod/lti/OAuthBody.php index 565cdd34163..5f43f69d39e 100644 --- a/mod/lti/OAuthBody.php +++ b/mod/lti/OAuthBody.php @@ -114,7 +114,7 @@ function handleOAuthBodyPOST($oauth_consumer_key, $oauth_consumer_secret, $body, try { $server->verify_request($request); - } catch (Exception $e) { + } catch (\Exception $e) { $message = $e->getMessage(); throw new OAuthException("OAuth signature failed: " . $message); } diff --git a/mod/lti/backup/moodle2/backup_lti_stepslib.php b/mod/lti/backup/moodle2/backup_lti_stepslib.php index 8a7ac39bb1c..f74e29245c7 100644 --- a/mod/lti/backup/moodle2/backup_lti_stepslib.php +++ b/mod/lti/backup/moodle2/backup_lti_stepslib.php @@ -99,6 +99,9 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step // Define file annotations $lti->annotate_files('mod_lti', 'intro', null); // This file areas haven't itemid + // Add support for subplugin structure. + $this->add_subplugin_structure('ltisource', $lti, true); + // Return the root element (lti), wrapped into standard activity structure return $this->prepare_activity_structure($lti); } diff --git a/mod/lti/backup/moodle2/restore_lti_activity_task.class.php b/mod/lti/backup/moodle2/restore_lti_activity_task.class.php index 61113e00ed2..a1a1e92dedb 100644 --- a/mod/lti/backup/moodle2/restore_lti_activity_task.class.php +++ b/mod/lti/backup/moodle2/restore_lti_activity_task.class.php @@ -47,7 +47,7 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/lti/backup/moodle2/restore_lti_stepslib.php'); // Because it exists (must) +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 @@ -59,14 +59,14 @@ 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 + // 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 + // Label only has one structure step. $this->add_step(new restore_lti_activity_structure_step('lti_structure', 'lti.xml')); } @@ -129,4 +129,13 @@ class restore_lti_activity_task extends restore_activity_task { return $rules; } + + /** + * Getter for ltisource plugins. + * + * @return int + */ + public function get_old_moduleid() { + return $this->oldmoduleid; + } } diff --git a/mod/lti/backup/moodle2/restore_lti_stepslib.php b/mod/lti/backup/moodle2/restore_lti_stepslib.php index 042ac9f20f0..eef676be87f 100644 --- a/mod/lti/backup/moodle2/restore_lti_stepslib.php +++ b/mod/lti/backup/moodle2/restore_lti_stepslib.php @@ -56,9 +56,13 @@ class restore_lti_activity_structure_step extends restore_activity_structure_ste protected function define_structure() { $paths = array(); - $paths[] = new restore_path_element('lti', '/activity/lti'); + $lti = new restore_path_element('lti', '/activity/lti'); + $paths[] = $lti; - // Return the paths wrapped into standard activity structure + // Add support for subplugin structure. + $this->add_subplugin_structure('ltisource', $lti); + + // Return the paths wrapped into standard activity structure. return $this->prepare_activity_structure($paths); } @@ -78,12 +82,12 @@ class restore_lti_activity_structure_step extends restore_activity_structure_ste $newitemid = $DB->insert_record('lti', $data); - // immediately after inserting "activity" record, call this + // Immediately after inserting "activity" record, call this. $this->apply_activity_instance($newitemid); } protected function after_execute() { - // Add lti related files, no need to match by itemname (just internally handled context) + // Add lti related files, no need to match by itemname (just internally handled context). $this->add_related_files('mod_lti', 'intro', null); } } diff --git a/mod/lti/classes/plugininfo/ltisource.php b/mod/lti/classes/plugininfo/ltisource.php index d674c2392b6..7003fb04b7d 100644 --- a/mod/lti/classes/plugininfo/ltisource.php +++ b/mod/lti/classes/plugininfo/ltisource.php @@ -23,11 +23,50 @@ */ namespace mod_lti\plugininfo; -use core\plugininfo\base, core_plugin_manager, moodle_url; +use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); class ltisource extends base { - // Accept base class implementation 100%. + /** + * 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); + } + } } diff --git a/mod/lti/db/access.php b/mod/lti/db/access.php index 647b83f63d4..13a43157060 100644 --- a/mod/lti/db/access.php +++ b/mod/lti/db/access.php @@ -56,20 +56,6 @@ $capabilities = array( 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), - // Controls access to the grade.php script, which shows all the submissions - // made to the external tool that have been reported back to Moodle. - 'mod/lti:grade' => array( - 'riskbitmask' => RISK_PERSONAL, - - '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 given the Instructor role in the remote system, // otherwise they are given Learner. See the lti_get_ims_role function. diff --git a/mod/lti/edit_form.php b/mod/lti/edit_form.php index 558c1b4f76d..2b13b9e2a96 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -54,6 +54,8 @@ require_once($CFG->dirroot.'/mod/lti/locallib.php'); class mod_lti_edit_types_form extends moodleform{ public function definition() { + global $CFG; + $mform =& $this->_form; //------------------------------------------------------------------------------- @@ -96,6 +98,7 @@ class mod_lti_edit_types_form extends moodleform{ $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); @@ -137,7 +140,12 @@ class mod_lti_edit_types_form extends moodleform{ $mform->addElement('checkbox', 'lti_forcessl', ' ', ' ' . get_string('force_ssl', 'lti'), $options); $mform->setType('lti_forcessl', PARAM_BOOL); - $mform->setDefault('lti_forcessl', '0'); + 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)) { diff --git a/mod/lti/grade.php b/mod/lti/grade.php index c3c063d41dc..3dd793153a8 100644 --- a/mod/lti/grade.php +++ b/mod/lti/grade.php @@ -44,21 +44,17 @@ * @author Nikolas Galanis * @author Chris Scribner * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated since 2.8 */ -require_once("../../config.php"); -require_once($CFG->dirroot.'/mod/lti/lib.php'); -require_once($CFG->libdir.'/plagiarismlib.php'); +require_once(dirname(dirname(__DIR__)).'/config.php'); -$id = optional_param('id', 0, PARAM_INT); // Course module ID -$l = optional_param('l', 0, PARAM_INT); // lti instance ID -$mode = optional_param('mode', 'all', PARAM_ALPHA); // What mode are we in? -$download = optional_param('download' , 'none', PARAM_ALPHA); //ZIP download asked for? +$id = optional_param('id', 0, PARAM_INT); +$l = optional_param('l', 0, PARAM_INT); -if ($l) { // Two ways to specify the module +if ($l) { $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); @@ -67,100 +63,9 @@ if ($l) { // Two ways to specify the module $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); require_login($course, false, $cm); -$context = context_module::instance($cm->id); -require_capability('mod/lti:grade', $context); +require_capability('mod/lti:view', context_module::instance($cm->id)); -$url = new moodle_url('/mod/lti/grade.php', array('id' => $cm->id)); -if ($mode !== 'all') { - $url->param('mode', $mode); -} -$PAGE->set_url($url); +debugging('This file has been deprecated. Links to this file should automatically '. + 'fallback to /mod/lti/view.php once this file has been deleted.', DEBUG_DEVELOPER); -$module = array( - 'name' => 'mod_lti_submissions', - 'fullpath' => '/mod/lti/submissions.js', - 'requires' => array('base', 'yui2-datatable'), - 'strings' => array(), -); - -$PAGE->requires->js_init_call('M.mod_lti.submissions.init', array(), true, $module); - -$submissionquery = ' - SELECT s.id, u.firstname, u.lastname, u.id AS userid, s.datesubmitted, s.gradepercent - FROM {lti_submission} s - INNER JOIN {user} u ON s.userid = u.id - WHERE s.ltiid = :ltiid - ORDER BY s.datesubmitted DESC -'; - -$submissions = $DB->get_records_sql($submissionquery, array('ltiid' => $lti->id)); - -$html = ' - - - -'; - -$rowtemplate = ' - - - - - - - - - - - -'; - -$rows = ''; - -foreach ($submissions as $submission) { - $row = $rowtemplate; - - foreach ($submission as $key => $value) { - if ($key === 'datesubmitted') { - $value = userdate($value); - } - - $row = str_replace('', $value, $row); - } - - $rows .= $row; -} - -$table = str_replace('', $rows, $html); - -$title = get_string('submissionsfor', 'lti', $lti->name); - -$PAGE->set_title($title); -$PAGE->set_heading($course->fullname); - -echo $OUTPUT->header(); -echo $OUTPUT->heading(format_string($lti->name, true, array('context' => $context))); -echo $OUTPUT->heading(get_string('submissions', 'lti'), 3); - -echo $table; - -echo $OUTPUT->footer(); +redirect(new moodle_url('/mod/lti/view.php', array('l' => $lti->id))); diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 10ed43a1c94..4c407005d36 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -111,11 +111,11 @@ $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 environemnt. +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 neavigation controls +* **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. @@ -147,6 +147,7 @@ $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['errormisconfig'] = 'Misconfigured tool. Please ask your Moodle administrator to fix the configuration of the tool.'; +$string['existing_window'] = 'Existing window'; $string['extensions'] = 'LTI Extension Services'; $string['external_tool_type'] = 'External tool type'; $string['external_tool_type_help'] = 'The main purpose of a tool configuration is to set up a secure communication channel between Moodle and the tool provider. @@ -205,11 +206,11 @@ If you have selected a specific tool type, you may not need to enter a Launch UR 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 environemnt. +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 neavigation controls +* **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. @@ -222,6 +223,7 @@ $string['lti:grade'] = 'View grades returned by the external tool'; $string['lti:manage'] = 'Be an Instructor 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'] = 'LTI Administration'; $string['lti_errormsg'] = 'The tool returned the following error message: "{$a}"'; $string['lti_launch_error'] = 'An error occurred when launching the external tool:'; diff --git a/mod/lti/lib.php b/mod/lti/lib.php index 8903475fd52..4e7f162cae1 100644 --- a/mod/lti/lib.php +++ b/mod/lti/lib.php @@ -95,17 +95,20 @@ function lti_add_instance($lti, $mform) { $lti->timemodified = $lti->timecreated; $lti->servicesalt = uniqid('', true); + 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->grade)) { - $lti->grade = 100; // TODO: Why is this harcoded here and default @ DB + 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 ($lti->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS) { + if (isset($lti->instructorchoiceacceptgrades) && $lti->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS) { if (!isset($lti->cmidnumber)) { $lti->cmidnumber = ''; } @@ -139,13 +142,15 @@ function lti_update_instance($lti, $mform) { $lti->showdescriptionlaunch = 0; } - if (!isset($lti->grade)) { - $lti->grade = $DB->get_field('lti', 'grade', array('id' => $lti->id)); - } + lti_force_type_config_settings($lti, lti_get_type_config_by_instance($lti)); - if ($lti->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS) { + 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); } @@ -468,7 +473,7 @@ function lti_grade_item_delete($basiclti) { function lti_extend_settings_navigation($settings, $parentnode) { global $PAGE; - if (has_capability('mod/lti:grade', context_module::instance($PAGE->cm->id))) { + if (has_capability('mod/lti:manage', context_module::instance($PAGE->cm->id))) { $keys = $parentnode->get_children_key_list(); $node = navigation_node::create('Submissions', @@ -478,3 +483,21 @@ function lti_extend_settings_navigation($settings, $parentnode) { $parentnode->add_node($node, $keys[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'); +} diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 4b282cd8268..2bab6baa1f7 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -150,7 +150,7 @@ function lti_view($instance) { $orgid = $typeconfig['organizationid']; $course = $PAGE->course; - $requestparams = lti_build_request($instance, $typeconfig, $course); + $requestparams = lti_build_request($instance, $typeconfig, $course, $typeid); $launchcontainer = lti_get_launch_container($instance, $typeconfig); $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id); @@ -158,7 +158,11 @@ function lti_view($instance) { if ( $orgid ) { $requestparams["tool_consumer_instance_guid"] = $orgid; } - + if (!empty($CFG->mod_lti_institution_name)) { + $requestparams['tool_consumer_instance_name'] = $CFG->mod_lti_institution_name; + } else { + $requestparams['tool_consumer_instance_name'] = get_site()->fullname; + } if (empty($key) || empty($secret)) { $returnurlparams['unsigned'] = '1'; } @@ -171,8 +175,36 @@ function lti_view($instance) { $returnurl = lti_ensure_url_is_https($returnurl); } + $target = null; + 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 (!is_null($target)) { + $requestparams['launch_presentation_document_target'] = $target; + } + $requestparams['launch_presentation_return_url'] = $returnurl; + // 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)) { $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret); @@ -200,11 +232,22 @@ function lti_view($instance) { echo $content; } -function lti_build_sourcedid($instanceid, $userid, $launchid = null, $servicesalt) { +/** + * 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 { @@ -228,10 +271,11 @@ function lti_build_sourcedid($instanceid, $userid, $launchid = null, $servicesal * @param object $instance Basic LTI instance object * @param object $typeconfig Basic LTI tool configuration * @param object $course Course object + * @param int|null $typeid Basic LTI tool ID * * @return array $request Request details */ -function lti_build_request($instance, $typeconfig, $course) { +function lti_build_request($instance, $typeconfig, $course, $typeid = null) { global $USER, $CFG; if (empty($instance->cmid)) { @@ -252,22 +296,30 @@ function lti_build_request($instance, $typeconfig, $course) { 'launch_presentation_locale' => current_language() ); + if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { + $requestparams['resource_link_id'] = $instance->resource_link_id; + } $placementsecret = $instance->servicesalt; if ( isset($placementsecret) ) { - $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, null, $placementsecret)); + $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); + $requestparams['lis_result_sourcedid'] = $sourcedid; } if ( isset($placementsecret) && ( $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || ( $typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS ) ) ) { - $requestparams['lis_result_sourcedid'] = $sourcedid; //Add outcome service URL $serviceurl = new moodle_url('/mod/lti/service.php'); $serviceurl = $serviceurl->out(); - if ($typeconfig['forcessl'] == '1') { + $forcessl = false; + if (!empty($CFG->mod_lti_forcessl)) { + $forcessl = true; + } + + if ($typeconfig['forcessl'] == '1' or $forcessl) { $serviceurl = lti_ensure_url_is_https($serviceurl); } @@ -497,7 +549,7 @@ function lti_get_ims_role($user, $cmid, $courseid) { } if (is_siteadmin($user)) { - array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator'); + array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator'); } return join(',', $roles); @@ -638,6 +690,10 @@ function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STA } 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'] = ''; @@ -1173,3 +1229,103 @@ function lti_ensure_url_is_https($url) { 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'))) { + file_put_contents($tempfile, $rawbody); + 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]; + } + } +} + diff --git a/mod/lti/mod_form.js b/mod/lti/mod_form.js index aabce00bc93..6dc0e14f906 100644 --- a/mod/lti/mod_form.js +++ b/mod/lti/mod_form.js @@ -77,9 +77,25 @@ }, 2000); }); + var allowgrades = Y.one('#id_instructorchoiceacceptgrades'); + allowgrades.on('change', this.toggleGradeSection, this); + updateToolMatches(); }, + toggleGradeSection: function(e) { + if (e) { + e.preventDefault(); + } + var allowgrades = Y.one('#id_instructorchoiceacceptgrades'); + var gradefieldset = Y.one('#modstandardgrade'); + if (!allowgrades.get('checked')) { + gradefieldset.hide(); + } else { + gradefieldset.show(); + } + }, + clearToolCache: function(){ this.urlCache = {}; this.toolTypeCache = {}; @@ -138,9 +154,10 @@ automatchToolDisplay.set('innerHTML', '' + M.str.lti.custom_config); } - var continuation = function(toolInfo){ - self.updatePrivacySettings(toolInfo); - + var continuation = function(toolInfo, inputfield){ + if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) { + self.updatePrivacySettings(toolInfo); + } if(toolInfo.toolname){ automatchToolDisplay.set('innerHTML', '' + M.str.lti.using_tool_configuration + toolInfo.toolname); } else if(!selectedToolType) { @@ -159,7 +176,7 @@ return continuation(self.urlCache[url]); } else if(!selectedToolType && !url) { // No tool type or url set - return continuation({}); + return continuation({}, field); } else { self.findToolByUrl(url, selectedToolType, function(toolInfo){ if(toolInfo){ @@ -237,6 +254,8 @@ } } } + + this.toggleGradeSection(); }, getSelectedToolTypeOption: function(){ diff --git a/mod/lti/mod_form.php b/mod/lti/mod_form.php index c2322681275..e34eb239a0d 100644 --- a/mod/lti/mod_form.php +++ b/mod/lti/mod_form.php @@ -125,6 +125,7 @@ class mod_lti_mod_form extends moodleform_mod { $launchoptions[LTI_LAUNCH_CONTAINER_DEFAULT] = get_string('default', 'lti'); $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', 'launchcontainer', get_string('launchinpopup', 'lti'), $launchoptions); @@ -194,6 +195,9 @@ class mod_lti_mod_form extends moodleform_mod { } */ + // Add standard course module grading elements. + $this->standard_grading_coursemodule_elements(); + //------------------------------------------------------------------------------- // add standard elements, common to all modules $this->standard_coursemodule_elements(); diff --git a/mod/lti/service.php b/mod/lti/service.php index beae870997e..7cfb4258a5a 100644 --- a/mod/lti/service.php +++ b/mod/lti/service.php @@ -34,6 +34,10 @@ use moodle\mod\lti as lti; $rawbody = file_get_contents("php://input"); +if (lti_should_log_request($rawbody)) { + lti_log_request($rawbody); +} + foreach (lti\OAuthUtil::get_headers() as $name => $value) { if ($name === 'Authorization') { // TODO: Switch to core oauthlib once implemented - MDL-30149 @@ -78,7 +82,12 @@ switch ($messagetype) { $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); @@ -98,6 +107,10 @@ switch ($messagetype) { $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); @@ -127,7 +140,12 @@ switch ($messagetype) { $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); diff --git a/mod/lti/servicelib.php b/mod/lti/servicelib.php index 3d7cc0eb180..c1f3f97003d 100644 --- a/mod/lti/servicelib.php +++ b/mod/lti/servicelib.php @@ -79,11 +79,12 @@ function lti_parse_grade_replace_message($xml) { } $parsed = new stdClass(); - $parsed->gradeval = $grade * 100; + $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); @@ -99,6 +100,7 @@ function lti_parse_grade_read_message($xml) { $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); @@ -114,6 +116,7 @@ function lti_parse_grade_delete_message($xml) { $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); @@ -121,6 +124,33 @@ function lti_parse_grade_delete_message($xml) { return $parsed; } +function lti_accepts_grades($ltiinstance) { + $acceptsgrades = true; + $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; + } + + 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'); @@ -128,6 +158,8 @@ function lti_update_grade($ltiinstance, $userid, $launchid, $gradeval) { $params = array(); $params['itemname'] = $ltiinstance->name; + $gradeval = $gradeval * floatval($ltiinstance->grade); + $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = $gradeval; @@ -170,10 +202,12 @@ function lti_read_grade($ltiinstance, $userid) { $grades = grade_get_grades($ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, $userid); - if (isset($grades) && isset($grades->items[0]) && is_array($grades->items[0]->grades)) { + $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; - $grade = $grade / 100.0; + $grade = $grade / $ltigrade; break; } } @@ -191,7 +225,7 @@ function lti_delete_grade($ltiinstance, $userid) { $grade->userid = $userid; $grade->rawgrade = null; - $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade, array('deleted'=>1)); + $status = grade_update(LTI_SOURCE, $ltiinstance->course, LTI_ITEM_TYPE, LTI_ITEM_MODULE, $ltiinstance->id, 0, $grade); return $status == GRADE_UPDATE_OK; } @@ -215,8 +249,16 @@ function lti_verify_message($key, $sharedsecrets, $body, $headers = null) { 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, $parsed->launchid, $ltiinstance->servicesalt); + $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'); @@ -238,6 +280,7 @@ function lti_extend_lti_services($data) { 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); } catch (moodle_exception $e) { @@ -258,4 +301,4 @@ function lti_extend_lti_services($data) { return true; } return false; -} \ No newline at end of file +} diff --git a/mod/lti/settings.php b/mod/lti/settings.php index 20ca0625e94..bac2f4c74f4 100644 --- a/mod/lti/settings.php +++ b/mod/lti/settings.php @@ -48,6 +48,17 @@ 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); + +$ADMIN->add('modltifolder', $settings); + +foreach (core_plugin_manager::instance()->get_plugins_of_type('ltisource') as $plugin) { + /** @var \mod_lti\plugininfo\ltisource $plugin */ + $plugin->load_settings($ADMIN, 'modltifolder', $hassiteconfig); +} + if ($ADMIN->fulltree) { require_once($CFG->dirroot.'/mod/lti/locallib.php'); @@ -172,5 +183,16 @@ if ($ADMIN->fulltree) { //]] "; - $settings->add(new admin_setting_heading('lti_types', get_string('external_tool_types', 'lti') . $OUTPUT->help_icon('main_admin', 'lti'), $template)); + $settings->add(new admin_setting_heading('lti_types', new lang_string('external_tool_types', 'lti') . $OUTPUT->help_icon('main_admin', 'lti'), $template)); +} + +if (count($modltifolder->children) <= 1) { + // No need for a folder, revert to default activity settings page. + $ADMIN->prune('modltifolder'); +} else { + // Using the folder, update settings name. + $settings->visiblename = new lang_string('ltisettings', 'mod_lti'); + + // Tell core we already added the settings structure. + $settings = null; } diff --git a/mod/lti/styles.css b/mod/lti/styles.css index 0b252f28de3..0b677b693b9 100644 --- a/mod/lti/styles.css +++ b/mod/lti/styles.css @@ -15,19 +15,6 @@ .path-mod-lti .late {color: red;} .path-mod-lti .message {text-align: center;} -/** Styles for submissions.php **/ -#page-mod-lti-submissions fieldset.felement {margin-left: 16%;} -#page-mod-lti-submissions form#options div {text-align:right;margin-left:auto;margin-right:20px;} -#page-mod-lti-submissions .header .commands {display: inline;} -#page-mod-lti-submissions .picture {width: 35px;} -#page-mod-lti-submissions .fullname, -#page-mod-lti-submissions .timemodified, -#page-mod-lti-submissions .timemarked {text-align: left;} -#page-mod-lti-submissions .submissions .grade, -#page-mod-lti-submissions .submissions .outcome, -#page-mod-lti-submissions .submissions .finalgrade {text-align: right;} -#page-mod-lti-submissions .qgprefs #optiontable {text-align:right;margin-left:auto;} - /* Styles for admin */ .path-admin-mod-lti .mform .fitem .fitemtitle { min-width:18em;padding-right:1em } /* Prevent setting titles from wrapping */ diff --git a/mod/lti/submissions.js b/mod/lti/submissions.js deleted file mode 100644 index 2f4e730b5a6..00000000000 --- a/mod/lti/submissions.js +++ /dev/null @@ -1,74 +0,0 @@ -// 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 . - -/** - * Javascript extensions for LTI submission viewer. - * - * @package mod - * @subpackage 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 - */ -(function(){ - var Y; - - M.mod_lti = M.mod_lti || {}; - - M.mod_lti.submissions = { - init: function(yui3){ - if(yui3){ - Y = yui3; - } - - this.setupTable(); - }, - - setupTable: function(){ - var lti_submissions_table = Y.YUI2.util.Dom.get('lti_submissions_table'); - - var dataSource = new Y.YUI2.util.DataSource(lti_submissions_table); - - var configuredColumns = [ - { key: "user", label: "User", sortable:true }, - { key: "date", label: "Submission Date", sortable:true, formatter: 'date' }, - { key: "grade", - label: "Grade", - sortable:true, - formatter: function(cell, record, column, data){ - cell.innerHTML = parseFloat(data).toFixed(1) + '%'; - } - } - ]; - - dataSource.responseType = Y.YUI2.util.DataSource.TYPE_HTMLTABLE; - dataSource.responseSchema = { - fields: [ - { key: "user" }, - { key: "date", parser: "date" }, - { key: "grade", parser: "number" }, - ] - }; - - new Y.YUI2.widget.DataTable("lti_submissions_table_container", configuredColumns, dataSource, - { - sortedBy: {key:"date", dir:"desc"} - } - ); - - Y.one('#lti_submissions_table_container').setStyle('display', ''); - } - } -})(); diff --git a/mod/lti/tests/locallib_test.php b/mod/lti/tests/locallib_test.php index 33a21878bb7..b0a3b16f091 100644 --- a/mod/lti/tests/locallib_test.php +++ b/mod/lti/tests/locallib_test.php @@ -145,4 +145,19 @@ class mod_lti_locallib_testcase extends basic_testcase { $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() { + // 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?foo=bar')); + } } diff --git a/mod/lti/version.php b/mod/lti/version.php index 3e6719e4e06..b9db2d2e804 100644 --- a/mod/lti/version.php +++ b/mod/lti/version.php @@ -48,7 +48,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2014051200; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2014060200; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2014050800; // Requires this Moodle version $plugin->component = 'mod_lti'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; diff --git a/mod/lti/view.php b/mod/lti/view.php index e31adc7f052..d6e7f6eb61e 100644 --- a/mod/lti/view.php +++ b/mod/lti/view.php @@ -47,6 +47,7 @@ */ 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'); @@ -75,6 +76,9 @@ $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); + $url = new moodle_url('/mod/lti/view.php', array('id'=>$cm->id)); $PAGE->set_url($url); @@ -89,8 +93,6 @@ if ($launchcontainer == LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS) { $PAGE->set_pagelayout('incourse'); } -require_login($course); - // Mark viewed by user (if required). $completion = new completion_info($course); $completion->set_module_viewed($cm); @@ -135,31 +137,25 @@ if ( $launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW ) { $resize = '