MDL-54680 enrol_lti: Offer cartridges in LTI provider

This commit is contained in:
John Okely 2016-05-23 16:12:40 +08:00
parent 6f302b17b9
commit 3e9ab40361
14 changed files with 652 additions and 11 deletions

61
enrol/lti/cartridge.php Normal file
View File

@ -0,0 +1,61 @@
<?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/>.
/**
* Generates an XML IMS Cartridge with the details for the given tool
*
* @package enrol_lti
* @copyright 2016 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/lib/weblib.php');
$toolid = null;
$token = null;
$filearguments = get_file_argument();
$arguments = explode('/', trim($filearguments, '/'));
if (count($arguments) >= 2) { // Can put cartridge.xml at the end, or anything really.
list($toolid, $token) = $arguments;
}
$toolid = optional_param('id', $toolid, PARAM_INT);
$token = optional_param('token', $token, PARAM_ALPHANUM);
// Only show the cartridge if the token parameter is correct.
// If we do not compare with a shared secret, someone could very easily
// guess an id for the enrolment.
if (!\enrol_lti\helper::verify_tool_token($toolid, $token)) {
throw new \moodle_exception('incorrecttoken', 'enrol_lti');
}
$tool = \enrol_lti\helper::get_lti_tool($toolid);
if (!is_enabled_auth('lti')) {
print_error('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
} else if (!enrol_is_enabled('lti')) {
print_error('enrolisdisabled', 'enrol_lti');
} else if ($tool->status != ENROL_INSTANCE_ENABLED) {
print_error('enrolisdisabled', 'enrol_lti');
} else {
header('Content-Type: text/xml; charset=utf-8');
echo \enrol_lti\helper::create_cartridge($toolid);
}

View File

@ -380,4 +380,219 @@ class helper {
</imsx_POXBody>
</imsx_POXEnvelopeRequest>';
}
/**
* Returns the url to launch the lti tool.
*
* @param int $toolid the id of the shared tool
* @return moodle_url the url to launch the tool
* @since Moodle 3.2
*/
public static function get_launch_url($toolid) {
return new \moodle_url('/enrol/lti/tool.php', array('id' => $toolid));
}
/**
* Returns the name of the lti enrolment instance, or the name of the course/module being shared.
*
* @param stdClass $tool The lti tool
* @return string The name of the tool
* @since Moodle 3.2
*/
public static function get_name($tool) {
$name = null;
if (empty($tool->name)) {
$toolcontext = \context::instance_by_id($tool->contextid);
$name = $toolcontext->get_context_name();
} else {
$name = $tool->name;
};
return $name;
}
/**
* Returns a description of the course or module that this lti instance points to.
*
* @param stdClass $tool The lti tool
* @return string A description of the tool
* @since Moodle 3.2
*/
public static function get_description($tool) {
global $DB;
$description = '';
$context = \context::instance_by_id($tool->contextid);
if ($context->contextlevel == CONTEXT_COURSE) {
$course = $DB->get_record('course', array('id' => $context->instanceid));
$description = $course->summary;
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cmid = $context->instanceid;
$cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
$module = $DB->get_record($cm->modname, array('id' => $cm->instance));
$description = $module->intro;
}
return trim(html_to_text($description));
}
/**
* Returns the url to the cartridge representing the tool.
*
* If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
* If not it will be a php page with some parameters passed.
*
* @param stdClass $tool The lti tool
* @return string The url to the cartridge representing the tool
* @since Moodle 3.2
*/
public static function get_cartridge_url($tool) {
global $CFG;
$url = null;
$id = $tool->id;
$token = self::generate_tool_token($tool->id);
if ($CFG->slasharguments) {
$url = new \moodle_url('/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml');
} else {
$url = new \moodle_url('/enrol/lti/cartridge.php',
array(
'id' => $id,
'token' => $token
)
);
}
return $url;
}
/**
* Returns a unique hash for this site and this enrolment instance.
*
* Used to verify that the link to the cartridge has not just been guessed.
*
* @param int $toolid The id of the shared tool
* @return string MD5 hash of combined site ID and enrolment instance ID.
* @since Moodle 3.2
*/
public static function generate_tool_token($toolid) {
$siteidentifier = get_site_identifier();
$checkhash = md5($siteidentifier . '_enrol_lti_' . $toolid);
return $checkhash;
}
/**
* Verifies that the given token matches the token of the given shared tool.
*
* @param int $toolid The id of the shared tool
* @param string $token hash for this site and this enrolment instance
* @return boolean True if the token matches, false if it does not
* @since Moodle 3.2
*/
public static function verify_tool_token($toolid, $token) {
return $token == self::generate_tool_token($toolid);
}
/**
* Returns the parameters of the cartridge as an associative array of partial xpath.
*
* @param int $toolid The id of the shared tool
* @return array Recursive associative array with partial xpath to be concatenated into an xpath expression
* before setting the value.
* @since Moodle 3.2
*/
protected static function get_cartridge_parameters($toolid) {
global $OUTPUT, $PAGE, $SITE;
$PAGE->set_context(\context_system::instance());
// Get the tool.
$tool = self::get_lti_tool($toolid);
// Work out the name of the tool.
$title = self::get_name($tool);
$launchurl = self::get_launch_url($toolid);
$launchurl = $launchurl->out();
$icon = $OUTPUT->favicon();
$icon = $icon->out();
$securelaunchurl = null;
$secureicon = null;
$vendorurl = new \moodle_url('/');
$vendorurl = $vendorurl->out();
$description = self::get_description($tool);
// If we are a https site, we can add the launch url and icon urls as secure equivalents.
if (\is_https()) {
$securelaunchurl = $launchurl;
$secureicon = $icon;
}
return array(
"/cc:cartridge_basiclti_link" => array(
"/blti:title" => $title,
"/blti:description" => $description,
"/blti:extensions" => array(
"/lticm:property[@name='icon_url']" => $icon,
"/lticm:property[@name='secure_icon_url']" => $secureicon
),
"/blti:launch_url" => $launchurl,
"/blti:secure_launch_url" => $securelaunchurl,
"/blti:icon" => $icon,
"/blti:secure_icon" => $secureicon,
"/blti:vendor" => array(
"/lticp:code" => $SITE->shortname,
"/lticp:name" => $SITE->fullname,
"/lticp:description" => trim(html_to_text($SITE->summary)),
"/lticp:url" => $vendorurl
)
)
);
}
/**
* Traverses a recursive associative array, setting the properties of the corresponding
* xpath element.
*
* @param DOMXPath $xpath The xpath with the xml to modify
* @param array $parameters The array of xpaths to search through
* @param string $prefix The current xpath prefix (gets longer the deeper into the array you go)
* @return void
* @since Moodle 3.2
*/
protected static function set_xpath($xpath, $parameters, $prefix = '') {
foreach ($parameters as $key => $value) {
if (is_array($value)) {
self::set_xpath($xpath, $value, $prefix . $key);
} else {
$result = @$xpath->query($prefix . $key);
if ($result) {
$node = $result->item(0);
if ($node) {
if (is_null($value)) {
$node->parentNode->removeChild($node);
} else {
$node->nodeValue = $value;
}
}
} else {
throw new \coding_exception('Please check your XPATH and try again.');
}
}
}
}
/**
* Create an IMS cartridge for the tool.
*
* @param int $toolid The id of the shared tool
* @return string representing the generated cartridge
* @since Moodle 3.2
*/
public static function create_cartridge($toolid) {
$cartridge = new \DOMDocument();
$cartridge->load(realpath(__DIR__ . '/../xml/imslticc.xml'));
$xpath = new \DOMXpath($cartridge);
$xpath->registerNamespace('cc', 'http://www.imsglobal.org/xsd/imslticc_v1p0');
$parameters = self::get_cartridge_parameters($toolid);
self::set_xpath($xpath, $parameters);
return $cartridge->saveXML();
}
}

View File

@ -96,12 +96,7 @@ class manage_table extends \table_sql {
* @return string
*/
public function col_name($tool) {
if (empty($tool->name)) {
$toolcontext = \context::instance_by_id($tool->contextid);
$name = $toolcontext->get_context_name();
} else {
$name = $tool->name;
};
$name = helper::get_name($tool);
return $this->get_display_text($tool, $name);
}
@ -113,8 +108,9 @@ class manage_table extends \table_sql {
* @return string
*/
public function col_url($tool) {
$url = new \moodle_url('/enrol/lti/tool.php', array('id' => $tool->id));
return $this->get_display_text($tool, $url);
$url = helper::get_cartridge_url($tool);
return $this->get_copyable_text($tool, $url);
}
/**
@ -124,7 +120,7 @@ class manage_table extends \table_sql {
* @return string
*/
public function col_secret($tool) {
return $this->get_display_text($tool, $tool->secret);
return $this->get_copyable_text($tool, $tool->secret);
}
@ -215,4 +211,22 @@ class manage_table extends \table_sql {
return $text;
}
/**
* Returns text to display in the columns.
*
* @param \stdClass $tool the tool
* @param string $text the text to alter
* @return string
* @since Moodle 3.2
*/
protected function get_copyable_text($tool, $text) {
global $OUTPUT;
$copyable = $OUTPUT->render_from_template('core/copy_box', array('text' => $text));
if ($tool->status != ENROL_INSTANCE_ENABLED) {
return \html_writer::tag('span', $copyable, array('class' => 'dimmed_text', 'style' => 'overflow: scroll'));
}
return $copyable;
}
}

View File

@ -37,6 +37,7 @@ $string['enrolstartdate_help'] = 'If enabled, users can access from this date on
$string['frameembeddingnotenabled'] = 'To access the tool, please follow the link below.';
$string['gradesync'] = 'Grade synchronisation';
$string['gradesync_help'] = 'Whether grades from the tool are sent to the remote system (LTI consumer).';
$string['incorrecttoken'] = 'Token was incorrect please check the URL and try again, or contact the administrator of this tool.';
$string['maxenrolled'] = 'Maximum enrolled users';
$string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.';
$string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.';

4
enrol/lti/styles.css Normal file
View File

@ -0,0 +1,4 @@
.copy_box {
width: 100%;
max-width: 350px;
}

9
enrol/lti/tests/fixtures/input.xml vendored Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode></firstnode>
<parentnode>
<childnode></childnode>
</parentnode>
<ambiguous id="0"></ambiguous>
<ambiguous id="1"></ambiguous>
</root>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode/>
<parentnode>
<childnode/>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1">Content 1</ambiguous>
</root>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode>Content 1</firstnode>
<parentnode>
<childnode>Content 2</childnode>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1"/>
</root>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<firstnode>Content 1</firstnode>
<parentnode>
<childnode/>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1"/>
</root>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<parentnode>
</parentnode>
<ambiguous id="0"/>
<ambiguous id="1"/>
</root>

View File

@ -247,6 +247,233 @@ class enrol_lti_helper_testcase extends advanced_testcase {
$this->assertTrue(isset($tools[$tool3->id]));
}
/**
* Test getting the launch url of a tool
*/
public function test_get_launch_url() {
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$id = $tool1->id;
$launchurl = \enrol_lti\helper::get_launch_url($id);
$this->assertEquals('http://www.example.com/moodle/enrol/lti/tool.php?id=' . $id, $launchurl->out());
}
/**
* Test getting the cartridge url of a tool
*/
public function test_get_cartridge_url() {
global $CFG;
$slasharguments = $CFG->slasharguments;
$CFG->slasharguments = false;
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$id = $tool1->id;
$token = \enrol_lti\helper::generate_tool_token($id);
$launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
$this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php?id=' . $id . '&amp;token=' . $token,
$launchurl->out());
$CFG->slasharguments = true;
$launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
$this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml',
$launchurl->out());
$CFG->slasharguments = $slasharguments;
}
/**
* Test getting the name of a tool
*/
public function test_get_name() {
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$name = \enrol_lti\helper::get_name($tool1);
$this->assertEquals('Course: Test course 1', $name);
$tool1->name = 'Shared course';
$name = \enrol_lti\helper::get_name($tool1);
$this->assertEquals('Shared course', $name);
}
/**
* Test getting the description of a tool
*/
public function test_get_description() {
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$description = \enrol_lti\helper::get_description($tool1);
$this->assertContains('Test course 1 Lorem ipsum dolor sit amet', $description);
$module1 = $this->getDataGenerator()->create_module('assign', array(
'course' => $course1->id
));
$data = new stdClass();
$data->cmid = $module1->cmid;
$tool2 = $this->create_tool($data);
$description = \enrol_lti\helper::get_description($tool2);
$this->assertContains('Test assign 1', $description);
}
/**
* Test verifying a tool token.
*/
public function test_verify_tool_token() {
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$token = \enrol_lti\helper::generate_tool_token($tool1->id);
$this->assertTrue(\enrol_lti\helper::verify_tool_token($tool1->id, $token));
$this->assertFalse(\enrol_lti\helper::verify_tool_token($tool1->id, 'incorrect token!'));
}
/**
* Data provider for the set_xpath test
*/
public function set_xpath_provider() {
return [
"Correct structure" => [
"parameters" => [
"/root" => [
"/firstnode" => "Content 1",
"/parentnode" => [
"/childnode" => "Content 2"
]
]
],
"expected" => "test_correct_xpath-expected.xml"
],
"A null value, but no node to remove" => [
"parameters" => [
"/root" => [
"/nonexistant" => null,
"/firstnode" => "Content 1"
]
],
"expected" => "test_missing_node-expected.xml"
],
"A string value, but no node existing to set" => [
"parameters" => [
"/root" => [
"/nonexistant" => "This will not be set",
"/firstnode" => "Content 1"
]
],
"expected" => "test_missing_node-expected.xml"
],
"Array but no children exist" => [
"parameters" => [
"/root" => [
"/nonexistant" => [
"/alsononexistant" => "This will not be set"
],
"/firstnode" => "Content 1"
]
],
"expected" => "test_missing_node-expected.xml"
],
"Remove nodes" => [
"parameters" => [
"/root" => [
"/parentnode" => [
"/childnode" => null
],
"/firstnode" => null
]
],
"expected" => "test_nodes_removed-expected.xml"
],
"Get by attribute" => [
"parameters" => [
"/root" => [
"/ambiguous[@id='1']" => 'Content 1'
]
],
"expected" => "test_ambiguous_nodes-expected.xml"
]
];
}
/**
* Test set_xpath.
* @dataProvider set_xpath_provider
* @param array $parameters A hash of parameters represented by a heirarchy of xpath expressions
* @param string $expected The name of the fixture file containing the expected result.
*/
public function test_set_xpath($parameters, $expected) {
$helper = new ReflectionClass('enrol_lti\\helper');
$function = $helper->getMethod('set_xpath');
$function->setAccessible(true);
$document = new \DOMDocument();
$document->load(realpath(__DIR__ . '/fixtures/input.xml'));
$xpath = new \DOMXpath($document);
$function->invokeArgs(null, [$xpath, $parameters]);
$result = $document->saveXML();
$expected = file_get_contents(realpath(__DIR__ . '/fixtures/' . $expected));
$this->assertEquals($expected, $result);
}
/**
* Test set_xpath when an incorrect xpath expression is given.
* @expectedException coding_exception
*/
public function test_set_xpath_incorrect_xpath() {
$parameters = [
"/root" => [
"/firstnode" => null,
"/parentnode*&#^*#(" => [
"/childnode" => null
],
]
];
$helper = new ReflectionClass('enrol_lti\\helper');
$function = $helper->getMethod('set_xpath');
$function->setAccessible(true);
$document = new \DOMDocument();
$document->load(realpath(__DIR__ . '/fixtures/input.xml'));
$xpath = new \DOMXpath($document);
$function->invokeArgs(null, [$xpath, $parameters]);
$result = $document->saveXML();
$expected = file_get_contents(realpath(__DIR__ . '/fixtures/' . $expected));
$this->assertEquals($expected, $result);
}
/**
* Test create cartridge.
*/
public function test_create_cartridge() {
global $CFG;
$course1 = $this->getDataGenerator()->create_course();
$data = new stdClass();
$data->courseid = $course1->id;
$tool1 = $this->create_tool($data);
$cartridge = \enrol_lti\helper::create_cartridge($tool1->id);
$this->assertContains('<blti:title>Test LTI</blti:title>', $cartridge);
$this->assertContains("<blti:icon>$CFG->wwwroot/theme/image.php/_s/clean/theme/1/favicon</blti:icon>", $cartridge);
$this->assertContains("<blti:launch_url>$CFG->wwwroot/enrol/lti/tool.php?id=$tool1->id</blti:launch_url>", $cartridge);
}
/**
* Helper function used to create a tool.
*
@ -267,6 +494,12 @@ class enrol_lti_helper_testcase extends advanced_testcase {
$course = get_course($data->courseid);
}
if (!empty($data->cmid)) {
$data->contextid = context_module::instance($data->cmid)->id;
} else {
$data->contextid = context_course::instance($data->courseid)->id;
}
// Set it to enabled if no status was specified.
if (!isset($data->status)) {
$data->status = ENROL_INSTANCE_ENABLED;
@ -274,7 +507,6 @@ class enrol_lti_helper_testcase extends advanced_testcase {
// Add some extra necessary fields to the data.
$data->name = 'Test LTI';
$data->contextid = context_course::instance($data->courseid)->id;
$data->roleinstructor = $studentrole->id;
$data->rolelearner = $teacherrole->id;

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2016052301; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2016051900; // Requires this Moodle version (3.1)
$plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics).

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title></blti:title>
<blti:description></blti:description>
<blti:extensions platform="org.moodle.lms">
<lticm:property name="icon_url"></lticm:property>
<lticm:property name="secure_icon_url"></lticm:property>
</blti:extensions>
<blti:launch_url></blti:launch_url>
<blti:secure_launch_url></blti:secure_launch_url>
<blti:icon></blti:icon>
<blti:secure_icon></blti:secure_icon>
<blti:vendor>
<lticp:code></lticp:code>
<lticp:name></lticp:name>
<lticp:description></lticp:description>
<lticp:url></lticp:url>
</blti:vendor>
<test></test>
</cartridge_basiclti_link>

View File

@ -0,0 +1,41 @@
{{!
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/>.
}}
{{!
@template core/copy_box
Interface element to contain text that the user should copy. Will automaticaly select when clicked.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* text The content to be displayed ready for copying
Example context (json):
{ "text": "Copyable text"}
}}
<input type="text" class="copy_box" value="{{{ text }}}" readonly="readonly" id="copy_box-{{uniqid}}"/>
{{# js }}
require(['jquery', 'theme_bootstrapbase/bootstrap'], function($) {
$('#copy_box-{{uniqid}}').on('click', function() {
$(this).select();
});
});
{{/ js }}