MDL-44902: Several additions to External Tool (LTI)

* LTI service related changes:
** Fixing exceptions in OAuth library.
** Added new launch option, Existing window: replaces entire page with the LTI object.
** The LTI tool ID used to perform the launch is now sent with the LTI launch parameters.  This is sent back to Moodle on subsequent requests.
** Added $CFG->mod_lti_forcessl to force SSL on all LTI launches.
** Added new LTI launch parameter: tool_consumer_instance_name.  Default value is site full name, but can be customized with $CFG->mod_lti_institution_name.
** The LTI grade service endpoints now set the affected user to the session.  This was required for event listeners.
** Fix the grade deletion service.  Was deleting the grade item instead of just the grade.
** Send error response when LTI instance does not accept grades and grades are being sent.
** Added a method for writing incoming LTI requests to disk for debugging.  Disabled by default.
* Changes for ltisource plugins:
** Can now to plug into backup/restore.
** Can now have settings.php files.
** Can now hook into the LTI launch and edit parameters.
* Several grade changes:
** Added standard_grading_coursemodule_elements to LTI instance edit form.  This means LTI instances can be configured with a grade.
** No longer assumes that grade is out of 100.
** Replaced modl/lti:grade capability with mod/lti:view.
* JS on mod/lti/view.php for resizing the content object has been converted to YUI3.
* Fixed misspellings in language file.
* Added hooks for log post and view actions.
* Bug fix for lti_get_url_thumbprint() when the URL is missing a schema.
This commit is contained in:
Mark Nielsen 2014-04-01 15:07:54 -07:00
parent f500ff4e52
commit 8fa50fdd34
23 changed files with 759 additions and 74 deletions

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

118
mod/lti/classes/factory.php Normal file
View File

@ -0,0 +1,118 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Class Builder
*
* @package mod
* @subpackage lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
namespace mod_lti;
use coding_exception;
use mod_lti\observer\dispatcher;
/**
* Builds various classes
*
* @package mod_lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
class factory {
/**
* Given a component and class name suffix, create a full
* class name and ensure that it exists.
*
* Classes are namespace based.
*
* @param string $component The component to find the class file in
* @param string $suffix This is appended to the component name, together make the class name we want
* @return string
* @throws \coding_exception
*/
protected function build_class_name($component, $suffix) {
$classname = "\\$component\\$suffix";
if (!class_exists($classname)) {
throw new coding_exception("Expected to find $classname in the classes directory of $component");
}
return $classname;
}
/**
* Instantiate a class and optionally verify parent class
*
* @param string $class Create a new instance of this class
* @param null|string $parent Ensure that this is the parent class
* @return mixed
* @throws coding_exception
*/
protected function build_generic_instance($class, $parent = null) {
if (!is_null($parent)) {
$reflection = new \ReflectionClass($class);
if (!$reflection->isSubclassOf($parent)) {
throw new coding_exception("The $class must be a subclass of $parent");
}
}
return new $class();
}
/**
* Builds a single ltisource plugin listener
*
* @param string $component The component to find the class file in
* @return \mod_lti\observer\listener_interface
*/
public function build_listener($component) {
return $this->build_generic_instance(
$this->build_class_name($component, 'listener'),
'\mod_lti\observer\listener_interface'
);
}
/**
* Builds ltisource plugin listeners
*
* @return \mod_lti\observer\listener_interface[]
*/
public function build_listeners() {
$plugins = \core_component::get_plugin_list('ltisource');
$listeners = array();
foreach (array_keys($plugins) as $pluginname) {
try {
$listeners[] = $this->build_listener('ltisource_'.$pluginname);
} catch (\Exception $e) {
// Class is optional, so ignore if not found.
}
}
return $listeners;
}
/**
* Builds an event dispatcher with listeners.
*
* @return dispatcher
*/
public function build_dispatcher() {
$dispatcher = new dispatcher();
$dispatcher->set_listeners($this->build_listeners());
return $dispatcher;
}
}

View File

@ -0,0 +1,69 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Event for ltisource plugins.
*
* @package mod
* @subpackage lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
namespace mod_lti\observer;
/**
* This event occurs prior to an LTI launch.
*
* @package mod_lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
class before_launch_event {
/**
* LTI activity instance
*
* @var \stdClass
*/
public $instance;
/**
* Launch URL
*
* @var string
*/
public $endpoint;
/**
* Launch request parameters
*
* @var array
*/
public $params;
/**
* Constructor
*
* @param \stdClass $instance
* @param string $endpoint
* @param array $params
*/
public function __construct($instance, $endpoint, array $params) {
$this->instance = $instance;
$this->endpoint = $endpoint;
$this->params = $params;
}
}

View File

@ -0,0 +1,87 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Dispatches events to ltisource plugin listeners
*
* @package mod
* @subpackage lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
namespace mod_lti\observer;
/**
* Event dispatcher
*
* @package mod_lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
class dispatcher {
/**
* @var listener_interface[]
*/
protected $listeners = array();
/**
* Add a listener
*
* @param listener_interface $listener
*/
public function add_listener(listener_interface $listener) {
$this->listeners[] = $listener;
}
/**
* Set a list of listeners
*
* @param listener_interface[] $listeners
*/
public function set_listeners($listeners) {
$this->listeners = array();
foreach ($listeners as $listener) {
$this->add_listener($listener);
}
}
/**
* Dispatches an event.
*
* Very trivial right now.
*
* @param string $name Event name
* @param mixed $event The event object
* @throws \coding_exception
*/
public function dispatch($name, $event) {
foreach ($this->listeners as $listener) {
$subscribed = $listener->get_subscribed_events();
if (!array_key_exists($name, $subscribed)) {
continue;
}
$callable = array($listener, $subscribed[$name]);
if (!is_callable($callable)) {
throw new \coding_exception(
sprintf('The method %s is not callable on %s class', $subscribed[$name], get_class($listener))
);
}
call_user_func($callable, $event);
}
}
}

View File

@ -0,0 +1,44 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Defines internal events that ltisource
* plugins can subscribe to.
*
* @package mod
* @subpackage lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
namespace mod_lti\observer;
/**
* These are all of the event names
*
* @package mod_lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
final class events {
/**
* This is thrown before an LTI launch
*
* The event listener will recevie an instance
* of \mod_lti\observer\before_launch_event
*/
const BEFORE_LAUNCH = 'before.launch';
}

View File

@ -0,0 +1,47 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Listener interface for ltisource plugins
*
* @package mod
* @subpackage lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
namespace mod_lti\observer;
/**
* This interface allows a class to define the ltisource
* plugin events that the class would like to subscribe
* to
*
* @package mod_lti
* @copyright Copyright (c) 2009 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*/
interface listener_interface {
/**
* Register for events
*
* Example return:
* array('eventname' => 'methodToCall');
*
* @return array
*/
public function get_subscribed_events();
}

View File

@ -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.

View File

@ -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', '&nbsp;', ' ' . 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)) {

View File

@ -68,7 +68,10 @@ $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);
// Redirecting to lti view. May need to wrap this in config.
redirect(new moodle_url('/mod/lti/view.php', array('l' => $lti->id)));
$url = new moodle_url('/mod/lti/grade.php', array('id' => $cm->id));
if ($mode !== 'all') {

View File

@ -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.

View File

@ -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');
}

View File

@ -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,33 @@ 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;
$factory = new \mod_lti\factory();
$dispatcher = $factory->build_dispatcher();
$event = new \mod_lti\observer\before_launch_event($instance, $endpoint, $requestparams);
$dispatcher->dispatch(\mod_lti\observer\events::BEFORE_LAUNCH, $event);
// Allow params to be updated.
$requestparams = $event->params;
if (!empty($key) && !empty($secret)) {
$parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret);
@ -200,11 +229,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 +268,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 +293,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 +546,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 +687,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 +1226,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];
}
}
}

View File

@ -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', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + 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', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + 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(){

View File

@ -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();

View File

@ -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);

View File

@ -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;
}
}

View File

@ -173,4 +173,39 @@ if ($ADMIN->fulltree) {
</script>
";
$settings->add(new admin_setting_heading('lti_types', get_string('external_tool_types', 'lti') . $OUTPUT->help_icon('main_admin', 'lti'), $template));
if (!during_initial_install()) {
// Process subplugin settings pages if any.
// Every subplugin that wishes to have settings page should provide it's own
// settings.php assuming it will be added as a custom settings page.
// A type will be passed through subtype parameter.
// All such links will be placed in separate category called LTI.
$plugins = get_plugin_list('ltisource');
if (!empty($plugins)) {
$toadd = array();
foreach ($plugins as $name => $path) {
if (file_exists($path.DIRECTORY_SEPARATOR.'settings.php')) {
$toadd[] = $name;
}
}
if (!empty($toadd)) {
$ADMIN->add('modules',
new admin_category('ltisource',
new lang_string('lti', 'lti'),
$module->is_enabled() === false)
);
foreach ($toadd as $name) {
$component = 'ltisource_'.$name;
$ADMIN->add($component,
new admin_externalpage($name,
new lang_string('pluginname', $component),
new moodle_url("/mod/lti/source/{$name}/settings.php",
array('subtype' => $component)))
);
}
}
}
}
}

View File

@ -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'));
}
}

View File

@ -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 = '
<script type="text/javascript">
//<![CDATA[
YUI().use("yui2-dom", function(Y) {
YUI().use("node", "event", function(Y) {
//Take scrollbars off the outer document to prevent double scroll bar effect
document.body.style.overflow = "hidden";
var dom = Y.YUI2.util.Dom;
var frame = document.getElementById("contentframe");
var doc = Y.one("body");
doc.setStyle("overflow", "hidden");
var frame = Y.one("#contentframe");
var padding = 15; //The bottom of the iframe wasn\'t visible on some themes. Probably because of border widths, etc.
var lastHeight;
var resize = function(){
var viewportHeight = dom.getViewportHeight();
if(lastHeight !== Math.min(dom.getDocumentHeight(), viewportHeight)){
frame.style.height = viewportHeight - dom.getY(frame) - padding + "px";
lastHeight = Math.min(dom.getDocumentHeight(), dom.getViewportHeight());
var resize = function(e) {
var viewportHeight = doc.get("winHeight");
if(lastHeight !== Math.min(doc.get("docHeight"), viewportHeight)){
frame.setStyle("height", viewportHeight - frame.getY() - padding + "px");
lastHeight = Math.min(doc.get("docHeight"), doc.get("winHeight"));
}
};
resize();
setInterval(resize, 250);
Y.on("windowresize", resize);
});
//]]
</script>