diff --git a/mod/lti/amd/build/contentitem.min.js b/mod/lti/amd/build/contentitem.min.js new file mode 100644 index 00000000000..574f74cfa97 --- /dev/null +++ b/mod/lti/amd/build/contentitem.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/str","core/templates","mod_lti/form-field","core/yui"],function(a,b,c,d,e){var f,g={init:function(a,e){var g="";c.get_string("selectcontent","lti").then(function(b){g=b;var c={url:a,postData:e};return d.render("mod_lti/contentitem",c)}).then(function(a,c){f=new M.core.dialogue({modal:!0,headerContent:g,bodyContent:a,draggable:!0,width:"800px",height:"600px"}),f.show(),f.after("visibleChange",function(a){a.prevVal&&!a.newVal&&(this.destroy(),b.fetchNotifications())},f),d.runTemplateJS(c)}).fail(b.exception)}},h=[new e("name",e.TYPES.TEXT,(!1),""),new e("introeditor",e.TYPES.EDITOR,(!1),""),new e("toolurl",e.TYPES.TEXT,(!0),""),new e("securetoolurl",e.TYPES.TEXT,(!0),""),new e("instructorchoiceacceptgrades",e.TYPES.CHECKBOX,(!0),(!0)),new e("instructorchoicesendname",e.TYPES.CHECKBOX,(!0),(!0)),new e("instructorchoicesendemailaddr",e.TYPES.CHECKBOX,(!0),(!0)),new e("instructorcustomparameters",e.TYPES.TEXT,(!0),""),new e("icon",e.TYPES.TEXT,(!0),""),new e("secureicon",e.TYPES.TEXT,(!0),""),new e("launchcontainer",e.TYPES.SELECT,(!0),0)];return window.processContentItemReturnData=function(b){f&&f.hide();var c;for(c in h){var d=h[c],e=null;"undefined"!==a.type(b[d.name])&&(e=b[d.name]),d.setFieldValue(e)}},g}); \ No newline at end of file diff --git a/mod/lti/amd/build/contentitem_return.min.js b/mod/lti/amd/build/contentitem_return.min.js new file mode 100644 index 00000000000..6c08d1a888a --- /dev/null +++ b/mod/lti/amd/build/contentitem_return.min.js @@ -0,0 +1 @@ +define([],function(){return{init:function(a){window!=top&&parent.processContentItemReturnData(a)}}}); \ No newline at end of file diff --git a/mod/lti/amd/build/form-field.min.js b/mod/lti/amd/build/form-field.min.js new file mode 100644 index 00000000000..d71dc09b708 --- /dev/null +++ b/mod/lti/amd/build/form-field.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=function(a,b,c,d){this.name=a,this.id="id_"+this.name,this.selector="#"+this.id,this.type=b,this.resetIfUndefined=c,this.defaultValue=d};return b.TYPES={TEXT:1,SELECT:2,CHECKBOX:3,EDITOR:4},b.prototype.setFieldValue=function(c){if(null===c){if(!this.resetIfUndefined)return;c=this.defaultValue}switch(this.type){case b.TYPES.CHECKBOX:c?a(this.selector).prop("checked",!0):a(this.selector).prop("checked",!1);break;case b.TYPES.EDITOR:if("undefined"!==a.type(c.text)){var d=a(this.selector+"editable");d.length?d.html(c.text):"undefined"!=typeof tinyMCE&&tinyMCE.execInstanceCommand(this.id,"mceInsertContent",!1,c.text),a(this.selector).val(c.text)}break;default:a(this.selector).val(c)}},b}); \ No newline at end of file diff --git a/mod/lti/amd/src/contentitem.js b/mod/lti/amd/src/contentitem.js new file mode 100644 index 00000000000..cf1fe526332 --- /dev/null +++ b/mod/lti/amd/src/contentitem.js @@ -0,0 +1,122 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Launches the modal dialogue that contains the iframe that sends the Content-Item selection request to an + * LTI tool provider that supports Content-Item type message. + * + * See template: mod_lti/contentitem + * + * @module mod_lti/contentitem + * @class contentitem + * @package mod_lti + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['jquery', 'core/notification', 'core/str', 'core/templates', 'mod_lti/form-field', 'core/yui'], + function($, notification, str, templates, FormField) { + var dialogue; + var contentItem = { + /** + * Init function. + * + * @param {string} url The URL for the content item selection. + * @param {object} postData The data to be sent for the content item selection request. + */ + init: function(url, postData) { + var dialogueTitle = ''; + str.get_string('selectcontent', 'lti').then(function(title) { + dialogueTitle = title; + var context = { + url: url, + postData: postData + }; + return templates.render('mod_lti/contentitem', context); + + }).then(function(html, js) { + // Set dialog's body content. + dialogue = new M.core.dialogue({ + modal: true, + headerContent: dialogueTitle, + bodyContent: html, + draggable: true, + width: '800px', + height: '600px' + }); + + // Show dialog. + dialogue.show(); + + // Destroy after hiding. + dialogue.after('visibleChange', function(e) { + // Going from visible to hidden. + if (e.prevVal && !e.newVal) { + this.destroy(); + // Fetch notifications. + notification.fetchNotifications(); + } + }, dialogue); + + templates.runTemplateJS(js); + + }).fail(notification.exception); + } + }; + + /** + * Array of form fields for LTI tool configuration. + * + * @type {*[]} + */ + var ltiFormFields = [ + new FormField('name', FormField.TYPES.TEXT, false, ''), + new FormField('introeditor', FormField.TYPES.EDITOR, false, ''), + new FormField('toolurl', FormField.TYPES.TEXT, true, ''), + new FormField('securetoolurl', FormField.TYPES.TEXT, true, ''), + new FormField('instructorchoiceacceptgrades', FormField.TYPES.CHECKBOX, true, true), + new FormField('instructorchoicesendname', FormField.TYPES.CHECKBOX, true, true), + new FormField('instructorchoicesendemailaddr', FormField.TYPES.CHECKBOX, true, true), + new FormField('instructorcustomparameters', FormField.TYPES.TEXT, true, ''), + new FormField('icon', FormField.TYPES.TEXT, true, ''), + new FormField('secureicon', FormField.TYPES.TEXT, true, ''), + new FormField('launchcontainer', FormField.TYPES.SELECT, true, 0) + ]; + + /** + * Window function that can be called from mod_lti/contentitem_return to close the dialogue and process the return data. + * + * @param {object} returnData The fetched configuration data from the Content-Item selection dialogue. + */ + window.processContentItemReturnData = function(returnData) { + if (dialogue) { + dialogue.hide(); + } + + // Populate LTI configuration fields from return data. + var index; + for (index in ltiFormFields) { + var field = ltiFormFields[index]; + var value = null; + if ($.type(returnData[field.name]) !== 'undefined') { + value = returnData[field.name]; + } + field.setFieldValue(value); + } + }; + + return contentItem; + } +); diff --git a/mod/lti/amd/src/contentitem_return.js b/mod/lti/amd/src/contentitem_return.js new file mode 100644 index 00000000000..2ae3ceca6f7 --- /dev/null +++ b/mod/lti/amd/src/contentitem_return.js @@ -0,0 +1,40 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Processes the result of LTI tool creation from a Content-Item message type. + * + * @module mod_lti/contentitem_return + * @class contentitem_return + * @package mod_lti + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define([], function() { + return { + /** + * Init function. + * + * @param {string} returnData The returned data. + */ + init: function(returnData) { + if (window != top) { + // Send return data to be processed by the parent window. + parent.processContentItemReturnData(returnData); + } + } + }; +}); diff --git a/mod/lti/amd/src/form-field.js b/mod/lti/amd/src/form-field.js new file mode 100644 index 00000000000..3d7f445fbef --- /dev/null +++ b/mod/lti/amd/src/form-field.js @@ -0,0 +1,107 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * A module that enables the setting of form field values on the client side. + * + * @module mod_lti/form-field + * @class form-field + * @package mod_lti + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['jquery'], + function($) { + /** + * Form field class. + * + * @param {string} name Field name. + * @param {number} type The field type. + * @param {boolean} resetIfUndefined Flag to reset the field to the default value if undefined in the return data. + * @param {string|number|boolean} defaultValue The default value to use for the field. + * @constructor + */ + var FormField = function(name, type, resetIfUndefined, defaultValue) { + this.name = name; + this.id = 'id_' + this.name; + this.selector = '#' + this.id; + this.type = type; + this.resetIfUndefined = resetIfUndefined; + this.defaultValue = defaultValue; + }; + + /** + * Form field types. + * + * @type {{TEXT: number, SELECT: number, CHECKBOX: number, EDITOR: number}} + */ + FormField.TYPES = { + TEXT: 1, + SELECT: 2, + CHECKBOX: 3, + EDITOR: 4 + }; + + /** + * Sets the values for a form field. + * + * @param {string|boolean|number} value The value to be set into the field. + */ + FormField.prototype.setFieldValue = function(value) { + if (value === null) { + if (this.resetIfUndefined) { + value = this.defaultValue; + } else { + // No need set the field value if value is null and there's no need to reset the field. + return; + } + } + + switch (this.type) { + case FormField.TYPES.CHECKBOX: + if (value) { + $(this.selector).prop('checked', true); + } else { + $(this.selector).prop('checked', false); + } + break; + case FormField.TYPES.EDITOR: + if ($.type(value.text) !== 'undefined') { + /* global tinyMCE:false */ + + // Set text in editor's editable content, if applicable. + // Check if it is an Atto editor. + var attoEditor = $(this.selector + 'editable'); + if (attoEditor.length) { + attoEditor.html(value.text); + } else if (typeof tinyMCE !== 'undefined') { + // If the editor is not Atto, try to fallback to TinyMCE. + tinyMCE.execInstanceCommand(this.id, 'mceInsertContent', false, value.text); + } + + // Set text to actual editor text area. + $(this.selector).val(value.text); + } + break; + default: + $(this.selector).val(value); + break; + } + }; + + return FormField; + } +); diff --git a/mod/lti/contentitem.php b/mod/lti/contentitem.php new file mode 100644 index 00000000000..0b6b87281d5 --- /dev/null +++ b/mod/lti/contentitem.php @@ -0,0 +1,56 @@ +. + +/** + * Handle sending a user to a tool provider to initiate a content-item selection. + * + * @package mod_lti + * @copyright 2015 Vital Source Technologies http://vitalsource.com + * @author Stephen Vickers + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/lti/lib.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +$id = required_param('id', PARAM_INT); +$courseid = required_param('course', PARAM_INT); +$title = optional_param('title', '', PARAM_TEXT); +$text = optional_param('text', '', PARAM_RAW); + +// Check access and capabilities. +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); +require_login($course); +$context = context_course::instance($courseid); +require_capability('moodle/course:manageactivities', $context); +require_capability('mod/lti:addcoursetool', $context); + +// Set the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. +$returnurlparams = [ + 'course' => $course->id, + 'id' => $id, + 'sesskey' => sesskey() +]; +$returnurl = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams); + +// Prepare the request. +$request = lti_build_content_item_selection_request($id, $course, $returnurl, $title, $text, [], []); + +// Get the launch HTML. +$content = lti_post_launch_html($request->params, $request->url, false); + +echo $content; diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php new file mode 100644 index 00000000000..53cc5de0195 --- /dev/null +++ b/mod/lti/contentitem_return.php @@ -0,0 +1,76 @@ +. + +/** + * Handle the return from the Tool Provider after selecting a content item. + * + * @package mod_lti + * @copyright 2015 Vital Source Technologies http://vitalsource.com + * @author Stephen Vickers + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +$id = required_param('id', PARAM_INT); +$courseid = required_param('course', PARAM_INT); +$messagetype = required_param('lti_message_type', PARAM_TEXT); +$version = required_param('lti_version', PARAM_TEXT); +$consumerkey = required_param('oauth_consumer_key', PARAM_RAW); +$items = optional_param('content_items', '', PARAM_RAW); +$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT); +$msg = optional_param('lti_msg', '', PARAM_TEXT); + +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); +require_login($course); +require_sesskey(); +$context = context_course::instance($courseid); +require_capability('moodle/course:manageactivities', $context); +require_capability('mod/lti:addcoursetool', $context); + +$redirecturl = null; +$returndata = null; +if (empty($errormsg) && !empty($items)) { + try { + $returndata = lti_tool_configuration_from_content_item($id, $messagetype, $version, $consumerkey, $items); + } catch (moodle_exception $e) { + $errormsg = $e->getMessage(); + } +} + +$pageurl = new moodle_url('/mod/lti/contentitem_return.php'); +$PAGE->set_url($pageurl); +$PAGE->set_pagelayout('popup'); +echo $OUTPUT->header(); + +// Call JS module to redirect the user to the course page or close the dialogue on error/cancel. +$PAGE->requires->js_call_amd('mod_lti/contentitem_return', 'init', [$returndata]); + +echo $OUTPUT->footer(); + +// Add messages to notification stack for rendering later. +if ($errormsg) { + // Content item selection has encountered an error. + \core\notification::error($errormsg); + +} else if (!empty($returndata)) { + // Means success. + if (!$msg) { + $msg = get_string('successfullyfetchedtoolconfigurationfromcontent', 'lti'); + } + \core\notification::success($msg); +} diff --git a/mod/lti/db/install.xml b/mod/lti/db/install.xml index 2fbbec06f32..040f32edae3 100644 --- a/mod/lti/db/install.xml +++ b/mod/lti/db/install.xml @@ -95,7 +95,7 @@ - + diff --git a/mod/lti/db/upgrade.php b/mod/lti/db/upgrade.php index 01d3131097f..ce19319d830 100644 --- a/mod/lti/db/upgrade.php +++ b/mod/lti/db/upgrade.php @@ -197,5 +197,20 @@ function xmldb_lti_upgrade($oldversion) { // Moodle v3.1.0 release upgrade line. // Put any upgrade step following this. + // Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + if ($oldversion < 2016052301) { + + // Changing type of field value on table lti_types_config to text. + $table = new xmldb_table('lti_types_config'); + $field = new xmldb_field('value', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'name'); + + // Launch change of type for field value. + $dbman->change_field_type($table, $field); + + // Lti savepoint reached. + upgrade_mod_savepoint(true, 2016052301, 'lti'); + } + return true; } diff --git a/mod/lti/edit_form.php b/mod/lti/edit_form.php index b667e2dd010..8db9ca53c95 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -140,6 +140,13 @@ class mod_lti_edit_types_form extends moodleform{ $mform->addHelpButton('lti_launchcontainer', 'default_launch_container', 'lti'); $mform->setType('lti_launchcontainer', PARAM_INT); + $mform->addElement('advcheckbox', 'lti_contentitem', get_string('contentitem', 'lti')); + $mform->addHelpButton('lti_contentitem', 'contentitem', 'lti'); + $mform->setAdvanced('lti_contentitem'); + if ($istool) { + $mform->disabledIf('lti_contentitem', null); + } + $mform->addElement('hidden', 'oldicon'); $mform->setType('oldicon', PARAM_URL); @@ -232,4 +239,18 @@ class mod_lti_edit_types_form extends moodleform{ $this->add_action_buttons(); } + + /** + * Retrieves the data of the submitted form. + * + * @return stdClass + */ + public function get_data() { + $data = parent::get_data(); + if ($data && !empty($this->_customdata->istool)) { + // Content item checkbox is disabled in tool settings, so this cannot be edited. Just unset it. + unset($data->lti_contentitem); + } + return $data; + } } diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 86d64c6ec11..cc7df136075 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -105,8 +105,10 @@ $string['configtoolurl'] = 'Default remote tool URL'; $string['configtypes'] = 'Enable LTI applications'; $string['configured'] = 'Configured'; $string['confirmtoolactivation'] = 'Are you sure you would like to activate this tool?'; -$string['courseactivitiesorresources'] = 'Course activities or resources'; +$string['contentitem'] = 'Content-Item Message'; +$string['contentitem_help'] = 'If ticked, the option \'Configure tool from link\' will be available when adding an external tool.'; $string['course_tool_types'] = 'Course tools'; +$string['courseactivitiesorresources'] = 'Course activities or resources'; $string['courseid'] = 'Course ID number'; $string['courseinformation'] = 'Course information'; $string['courselink'] = 'Go to course'; @@ -165,7 +167,12 @@ $string['enableemailnotification'] = 'Send notification emails'; $string['enableemailnotification_help'] = 'If enabled, students will receive email notification when their tool submissions are graded.'; $string['enterkeyandsecret'] = 'Enter your consumer key and shared secret'; $string['errorbadurl'] = 'URL is not a valid tool URL or cartridge.'; +$string['errorincorrectconsumerkey'] = 'Consumer key is incorrect.'; +$string['errorinvaliddata'] = 'Invalid data: {$a}'; +$string['errorinvalidmediatype'] = 'Invalid media type: {$a}'; +$string['errorinvalidresponseformat'] = 'Invalid Content-Item response format.'; $string['errormisconfig'] = 'Misconfigured tool. Please ask your Moodle administrator to fix the configuration of the tool.'; +$string['errortooltypenotfound'] = 'LTI tool type not found.'; $string['existing_window'] = 'Existing window'; $string['extensions'] = 'LTI extension services'; $string['external_tool_type'] = 'Preconfigured tool'; @@ -403,6 +410,7 @@ $string['secure_launch_url'] = 'Secure launch URL'; $string['secure_launch_url_help'] = 'Similar to the launch URL, but used instead of the launch URL if high security is required. Moodle will use the secure launch URL instead of the launch URL if the Moodle site is accessed through SSL, or if the tool configuration is set to always launch through SSL. The launch URL may also be set to an https address to force launching through SSL, and this field may be left blank.'; +$string['selectcontent'] = 'Select content'; $string['send'] = 'Send'; $string['services'] = 'Services'; $string['services_help'] = 'Select those services which you wish to offer to the tool provider. More than one service can be selected.'; @@ -444,6 +452,7 @@ $string['submission'] = 'Submission'; $string['submissions'] = 'Submissions'; $string['submissionsfor'] = 'Submissions for {$a}'; $string['successfullycreatedtooltype'] = 'Successfully created new tool!'; +$string['successfullyfetchedtoolconfigurationfromcontent'] = 'Successfully fetched tool configuration from the selected content.'; $string['subplugintype_ltiresource'] = 'LTI service resource'; $string['subplugintype_ltiresource_plural'] = 'LTI service resources'; $string['subplugintype_ltiservice'] = 'LTI service'; diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index ddec86456c4..ece6c9f41ab 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -55,6 +55,8 @@ use moodle\mod\lti as lti; require_once($CFG->dirroot.'/mod/lti/OAuth.php'); require_once($CFG->libdir.'/weblib.php'); +require_once($CFG->dirroot . '/course/modlib.php'); +require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i'); @@ -83,6 +85,9 @@ define('LTI_COURSEVISIBLE_NO', 0); define('LTI_COURSEVISIBLE_PRECONFIGURED', 1); define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2); +define('LTI_VERSION_1', 'LTI-1p0'); +define('LTI_VERSION_2', 'LTI-2p0'); + /** * Return the launch data required for opening the external tool. * @@ -376,18 +381,7 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2); - $intro = ''; - if (!empty($instance->cmid)) { - $intro = format_module_intro('lti', $instance, $instance->cmid); - $intro = html_to_text($intro, 0, false); - - // This may look weird, but this is required for new lines - // so we generate the same OAuth signature as the tool provider. - $intro = str_replace("\n", "\r\n", $intro); - } $requestparams = array( - 'resource_link_title' => $instance->name, - 'resource_link_description' => $intro, 'user_id' => $USER->id, 'lis_person_sourcedid' => $USER->idnumber, 'roles' => $role, @@ -395,6 +389,18 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl 'context_label' => $course->shortname, 'context_title' => $course->fullname, ); + if (!empty($instance->name)) { + $requestparams['resource_link_title'] = $instance->name; + } + if (!empty($instance->cmid)) { + $intro = format_module_intro('lti', $instance, $instance->cmid); + $intro = html_to_text($intro, 0, false); + + // This may look weird, but this is required for new lines + // so we generate the same OAuth signature as the tool provider. + $intro = str_replace("\n", "\r\n", $intro); + $requestparams['resource_link_description'] = $intro; + } if (!empty($instance->id)) { $requestparams['resource_link_id'] = $instance->id; } @@ -407,12 +413,12 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl $requestparams['context_type'] = 'CourseSection'; $requestparams['lis_course_section_sourcedid'] = $course->idnumber; } - $placementsecret = $instance->servicesalt; - - if ( !empty($instance->id) && isset($placementsecret) && ($islti2 || - $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || - ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) { + if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 || + $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || + ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS)) + ) { + $placementsecret = $instance->servicesalt; $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); $requestparams['lis_result_sourcedid'] = $sourcedid; @@ -434,7 +440,9 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl // Send user's name and email data if appropriate. if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS || - ( $typeconfig['sendname'] == LTI_SETTING_DELEGATE && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS ) ) { + ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname) + && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS) + ) { $requestparams['lis_person_name_given'] = $USER->firstname; $requestparams['lis_person_name_family'] = $USER->lastname; $requestparams['lis_person_name_full'] = $USER->firstname . ' ' . $USER->lastname; @@ -442,7 +450,9 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl } if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS || - ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS)) { + ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr) + && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS) + ) { $requestparams['lis_person_contact_email_primary'] = $USER->email; } @@ -481,20 +491,23 @@ function lti_build_request_lti2($tool, $params) { /** * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer * - * @param object $instance Basic LTI instance object + * @param stdClass $instance Basic LTI instance object * @param string $orgid Organisation ID * @param boolean $islti2 True if an LTI 2 tool is being launched + * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Request details */ -function lti_build_standard_request($instance, $orgid, $islti2) { +function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') { global $CFG; $requestparams = array(); - $requestparams['resource_link_id'] = $instance->id; - if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { - $requestparams['resource_link_id'] = $instance->resource_link_id; + if ($instance) { + $requestparams['resource_link_id'] = $instance->id; + if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { + $requestparams['resource_link_id'] = $instance->resource_link_id; + } } $requestparams['launch_presentation_locale'] = current_language(); @@ -512,7 +525,7 @@ function lti_build_standard_request($instance, $orgid, $islti2) { } else { $requestparams['lti_version'] = 'LTI-2p0'; } - $requestparams['lti_message_type'] = 'basic-lti-launch-request'; + $requestparams['lti_message_type'] = $messagetype; if ($orgid) { $requestparams["tool_consumer_instance_guid"] = $orgid; @@ -560,15 +573,356 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus $tool->parameter, true), $custom); $settings = lti_get_tool_settings($tool->toolproxyid); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); - $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course); - $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); - $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id); - $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); + if (!empty($instance->course)) { + $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course); + $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); + if (!empty($instance->id)) { + $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id); + $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); + } + } } return $custom; } +/** + * Builds a standard LTI Content-Item selection request. + * + * @param int $id The tool type ID. + * @param stdClass $course The course object. + * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP) + * will use to return the Content-Item message. + * @param string $title The tool's title, if available. + * @param string $text The text to display to represent the content item. This value may be a long description of the content item. + * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default. + * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened + * (via the presentationDocumentTarget element for a returned content item). + * If empty, "frame", "iframe", and "window" will be supported by default. + * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without + * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default. + * any option for the user to cancel the operation. False by default. + * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not. + * A signed message should always be required when the content item is being created automatically in the + * TC without further interaction from the user. False by default. + * @param bool $canconfirm Flag for can_confirm parameter. False by default. + * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default. + * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface. + * @throws moodle_exception When the LTI tool type does not exist.` + * @throws coding_exception For invalid media type and presentation target parameters. + */ +function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [], + $presentationtargets = [], $autocreate = false, $multiple = false, + $unsigned = false, $canconfirm = false, $copyadvice = false) { + $tool = lti_get_type($id); + // Validate parameters. + if (!$tool) { + throw new moodle_exception('errortooltypenotfound', 'mod_lti'); + } + if (!is_array($mediatypes)) { + throw new coding_exception('The list of accepted media types should be in an array'); + } + if (!is_array($presentationtargets)) { + throw new coding_exception('The list of accepted presentation targets should be in an array'); + } + + // Check title. If empty, use the tool's name. + if (empty($title)) { + $title = $tool->name; + } + + $typeconfig = lti_get_type_config($id); + $key = ''; + $secret = ''; + $islti2 = false; + if (isset($tool->toolproxyid)) { + $islti2 = true; + $toolproxy = lti_get_tool_proxy($tool->toolproxyid); + $key = $toolproxy->guid; + $secret = $toolproxy->secret; + } else { + $toolproxy = null; + if (!empty($typeconfig['resourcekey'])) { + $key = $typeconfig['resourcekey']; + } + if (!empty($typeconfig['password'])) { + $secret = $typeconfig['password']; + } + } + $tool->enabledcapability = ''; + if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) { + $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest']; + } + + $tool->parameter = ''; + if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) { + $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest']; + } + + // Set the tool URL. + if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) { + $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']); + } else { + $toolurl = new moodle_url($typeconfig['toolurl']); + } + + // Check if SSL is forced. + if (!empty($typeconfig['forcessl'])) { + // Make sure the tool URL is set to https. + if (strtolower($toolurl->get_scheme()) === 'http') { + $toolurl->set_scheme('https'); + } + // Make sure the return URL is set to https. + if (strtolower($returnurl->get_scheme()) === 'http') { + $returnurl->set_scheme('https'); + } + } + $toolurlout = $toolurl->out(false); + + // Get base request parameters. + $instance = new stdClass(); + $instance->course = $course->id; + $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2); + + // Get LTI2-specific request parameters and merge to the request parameters if applicable. + if ($islti2) { + $lti2params = lti_build_request_lti2($tool, $requestparams); + $requestparams = array_merge($requestparams, $lti2params); + } + + // Get standard request parameters and merge to the request parameters. + $orgid = !empty($typeconfig['organizationid']) ? $typeconfig['organizationid'] : ''; + $standardparams = lti_build_standard_request(null, $orgid, $islti2, 'ContentItemSelectionRequest'); + $requestparams = array_merge($requestparams, $standardparams); + + // Get custom request parameters and merge to the request parameters. + $customstr = ''; + if (!empty($typeconfig['customparameters'])) { + $customstr = $typeconfig['customparameters']; + } + $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2); + $requestparams = array_merge($requestparams, $customparams); + + // Allow request params to be updated by sub-plugins. + $plugins = core_component::get_plugin_list('ltisource'); + foreach (array_keys($plugins) as $plugin) { + $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []); + + if (!empty($pluginparams) && is_array($pluginparams)) { + $requestparams = array_merge($requestparams, $pluginparams); + } + } + + // Media types. Set to ltilink by default if empty. + if (empty($mediatypes)) { + $mediatypes = [ + 'application/vnd.ims.lti.v1.ltilink', + ]; + } + $requestparams['accept_media_types'] = implode(',', $mediatypes); + + // Presentation targets. Supports frame, iframe, window by default if empty. + if (empty($presentationtargets)) { + $presentationtargets = [ + 'frame', + 'iframe', + 'window', + ]; + } + $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets); + + // Other request parameters. + $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false'; + $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false'; + $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false'; + $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false'; + $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false'; + $requestparams['content_item_return_url'] = $returnurl->out(false); + $requestparams['title'] = $title; + $requestparams['text'] = $text; + $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret); + $toolurlparams = $toolurl->params(); + + // Strip querystring params in endpoint url from $signedparams to avoid duplication. + if (!empty($toolurlparams) && !empty($signedparams)) { + foreach (array_keys($toolurlparams) as $paramname) { + if (isset($signedparams[$paramname])) { + unset($signedparams[$paramname]); + } + } + } + + // Check for params that should not be passed. Unset if they are set. + $unwantedparams = [ + 'resource_link_id', + 'resource_link_title', + 'resource_link_description', + 'launch_presentation_return_url', + 'lis_result_sourcedid', + ]; + foreach ($unwantedparams as $param) { + if (isset($signedparams[$param])) { + unset($signedparams[$param]); + } + } + + // Prepare result object. + $result = new stdClass(); + $result->params = $signedparams; + $result->url = $toolurlout; + + return $result; +} + +/** + * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the + * selected content item. This configuration data can be then used when adding a tool into the course. + * + * @param int $typeid The tool type ID. + * @param string $messagetype The value for the lti_message_type parameter. + * @param string $ltiversion The value for the lti_version parameter. + * @param string $consumerkey The consumer key. + * @param string $contentitemsjson The JSON string for the content_items parameter. + * @return stdClass The array of module information objects. + * @throws moodle_exception + * @throws lti\OAuthException + */ +function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { + $tool = lti_get_type($typeid); + // Validate parameters. + if (!$tool) { + throw new moodle_exception('errortooltypenotfound', 'mod_lti'); + } + // Check lti_message_type. Show debugging if it's not set to ContentItemSelection. + // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment. + if ($messagetype !== 'ContentItemSelection') { + debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.", + DEBUG_DEVELOPER); + } + + $typeconfig = lti_get_type_config($typeid); + + if (isset($tool->toolproxyid)) { + $islti2 = true; + $toolproxy = lti_get_tool_proxy($tool->toolproxyid); + $key = $toolproxy->guid; + $secret = $toolproxy->secret; + } else { + $islti2 = false; + $toolproxy = null; + if (!empty($typeconfig['resourcekey'])) { + $key = $typeconfig['resourcekey']; + } else { + $key = ''; + } + if (!empty($typeconfig['password'])) { + $secret = $typeconfig['password']; + } else { + $secret = ''; + } + } + + // Check LTI versions from our side and the response's side. Show debugging if they don't match. + // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment. + $expectedversion = LTI_VERSION_1; + if ($islti2) { + $expectedversion = LTI_VERSION_2; + } + if ($ltiversion !== $expectedversion) { + debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," . + " Response: {$ltiversion}", DEBUG_DEVELOPER); + } + + if ($consumerkey !== $key) { + throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); + } + + $store = new lti\TrivialOAuthDataStore(); + $store->add_consumer($key, $secret); + $server = new lti\OAuthServer($store); + $method = new lti\OAuthSignatureMethod_HMAC_SHA1(); + $server->add_signature_method($method); + $request = lti\OAuthRequest::from_request(); + try { + $server->verify_request($request); + } catch (lti\OAuthException $e) { + throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage()); + } + + $items = json_decode($contentitemsjson); + if (empty($items)) { + throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson); + } + if ($items->{'@context'} !== 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem') { + throw new moodle_exception('errorinvalidmediatype', 'mod_lti', '', $items->{'@context'}); + } + if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) { + throw new moodle_exception('errorinvalidresponseformat', 'mod_lti'); + } + + $config = null; + if (!empty($items->{'@graph'})) { + $item = $items->{'@graph'}[0]; + + $config = new stdClass(); + $config->name = ''; + if (isset($item->title)) { + $config->name = $item->title; + } + if (empty($config->name)) { + $config->name = $tool->name; + } + if (isset($item->text)) { + $config->introeditor = [ + 'text' => $item->text, + 'format' => FORMAT_PLAIN + ]; + } + if (isset($item->icon->{'@id'})) { + $iconurl = new moodle_url($item->icon->{'@id'}); + // Assign item's icon URL to secureicon or icon depending on its scheme. + if (strtolower($iconurl->get_scheme()) === 'https') { + $config->secureicon = $iconurl->out(false); + } else { + $config->icon = $iconurl->out(false); + } + } + if (isset($item->url)) { + $url = new moodle_url($item->url); + // Assign item URL to securetoolurl or toolurl depending on its scheme. + if (strtolower($url->get_scheme()) === 'https') { + $config->securetoolurl = $url->out(false); + } else { + $config->toolurl = $url->out(false); + } + $config->typeid = 0; + } else { + $config->typeid = $typeid; + } + $config->instructorchoicesendname = LTI_SETTING_NEVER; + $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER; + $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER; + $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; + if (isset($item->placementAdvice->presentationDocumentTarget)) { + if ($item->placementAdvice->presentationDocumentTarget === 'window') { + $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW; + } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') { + $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; + } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') { + $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED; + } + } + if (isset($item->custom)) { + $customparameters = []; + foreach ($item->custom as $key => $value) { + $customparameters[] = "{$key}={$value}"; + } + $config->instructorcustomparameters = implode("\n", $customparameters); + } + } + return $config; +} + function lti_get_tool_table($tools, $id) { global $CFG, $OUTPUT, $USER; $html = ''; @@ -1454,6 +1808,10 @@ function lti_get_type_type_config($id) { $type->lti_coursevisible = $config['coursevisible']; } + if (isset($config['contentitem'])) { + $type->lti_contentitem = $config['contentitem']; + } + if (isset($config['debuglaunch'])) { $type->lti_debuglaunch = $config['debuglaunch']; } @@ -1489,6 +1847,10 @@ function lti_prepare_type_for_save($type, $config) { $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0; $config->lti_forcessl = $type->forcessl; + if (isset($config->lti_contentitem)) { + $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0; + $config->lti_contentitem = $type->contentitem; + } $type->timemodified = time(); @@ -2147,6 +2509,7 @@ function lti_get_capabilities() { $capabilities = array( 'basic-lti-launch-request' => '', + 'ContentItemSelectionRequest' => '', 'Context.id' => 'context_id', 'CourseSection.title' => 'context_title', 'CourseSection.label' => 'context_label', diff --git a/mod/lti/mod_form.js b/mod/lti/mod_form.js index 7318c275b8a..1044aed3991 100644 --- a/mod/lti/mod_form.js +++ b/mod/lti/mod_form.js @@ -49,7 +49,10 @@ self.updateAutomaticToolMatch(Y.one('#id_securetoolurl')); }; + var contentItemButton = Y.one('[name="selectcontent"]'); + var contentItemUrl = contentItemButton.getAttribute('data-contentitemurl'); var typeSelector = Y.one('#id_typeid'); + typeSelector.on('change', function(e){ updateToolMatches(); @@ -66,7 +69,28 @@ allowgrades.set('checked', !self.getSelectedToolTypeOption().getAttribute('nogrades')); self.toggleGradeSection(); } + }); + // Handle configure from link button click. + contentItemButton.on('click', function() { + var contentItemId = self.getContentItemId(); + if (contentItemId) { + // Get activity name and description values. + var title = Y.one('#id_name').get('value').trim(); + var text = Y.one('#id_introeditor').get('value').trim(); + + // Set data to be POSTed. + var postData = { + id: contentItemId, + course: self.settings.courseId, + title: title, + text: text + }; + + require(['mod_lti/contentitem'], function(contentitem) { + contentitem.init(contentItemUrl, postData); + }); + } }); this.createTypeEditorButtons(); @@ -492,7 +516,19 @@ } } }); - } + }, + /** + * Gets the tool type ID of the selected tool that supports Content-Item selection. + * + * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise. + */ + getContentItemId: function() { + var selected = this.getSelectedToolTypeOption(); + if (selected.getAttribute('data-contentitem')) { + return selected.getAttribute('data-id'); + } + return false; + } }; })(); diff --git a/mod/lti/mod_form.php b/mod/lti/mod_form.php index 91c55f19713..eea82d98b27 100644 --- a/mod/lti/mod_form.php +++ b/mod/lti/mod_form.php @@ -54,7 +54,7 @@ require_once($CFG->dirroot.'/mod/lti/locallib.php'); class mod_lti_mod_form extends moodleform_mod { public function definition() { - global $DB, $PAGE, $OUTPUT, $USER, $COURSE; + global $PAGE, $OUTPUT, $COURSE; if ($type = optional_param('type', false, PARAM_ALPHA)) { component_callback("ltisource_$type", 'add_instance_hook'); @@ -95,12 +95,20 @@ class mod_lti_mod_form extends moodleform_mod { $mform->addHelpButton('showdescriptionlaunch', 'display_description', 'lti'); // Tool settings. - $tooltypes = $mform->addElement('select', 'typeid', get_string('external_tool_type', 'lti'), array()); + $attributes = array(); + if ($update = optional_param('update', false, PARAM_INT)) { + $attributes['disabled'] = 'disabled'; + } + $attributes['class'] = 'lti_contentitem'; + $tooltypes = $mform->addElement('select', 'typeid', get_string('external_tool_type', 'lti'), array(), $attributes); $typeid = optional_param('typeid', false, PARAM_INT); $mform->getElement('typeid')->setValue($typeid); $mform->addHelpButton('typeid', 'external_tool_type', 'lti'); $toolproxy = array(); + // Array of tool type IDs that don't support ContentItemSelectionRequest. + $noncontentitemtypes = ['0']; + foreach (lti_get_types_for_add_instance() as $id => $type) { if (!empty($type->toolproxyid)) { $toolproxy[] = $type->id; @@ -123,20 +131,48 @@ class mod_lti_mod_form extends moodleform_mod { } else { $attributes = array(); } + if (!$update && $id) { + $config = lti_get_type_config($id); + if (!empty($config['contentitem'])) { + $attributes['data-contentitem'] = 1; + $attributes['data-id'] = $id; + } else { + $noncontentitemtypes[] = $id; + } + } $tooltypes->addOption($type->name, $id, $attributes); } + // Add button that launches the content-item selection dialogue. + + // Set contentitem URL. + $contentitemurl = new moodle_url('/mod/lti/contentitem.php'); + $contentbuttonattributes['data-contentitemurl'] = $contentitemurl->out(false); + $mform->addElement('button', 'selectcontent', get_string('selectcontent', 'lti'), $contentbuttonattributes); + if ($update) { + $mform->disabledIf('selectcontent', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('selectcontent', 'typeid', 'in', $noncontentitemtypes); + } $mform->addElement('text', 'toolurl', get_string('launch_url', 'lti'), array('size' => '64')); $mform->setType('toolurl', PARAM_URL); $mform->addHelpButton('toolurl', 'launch_url', 'lti'); - $mform->disabledIf('toolurl', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('toolurl', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('toolurl', 'typeid', 'in', $noncontentitemtypes); + } $mform->addElement('text', 'securetoolurl', get_string('secure_launch_url', 'lti'), array('size' => '64')); $mform->setType('securetoolurl', PARAM_URL); $mform->setAdvanced('securetoolurl'); $mform->addHelpButton('securetoolurl', 'secure_launch_url', 'lti'); - $mform->disabledIf('securetoolurl', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('securetoolurl', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('securetoolurl', 'typeid', 'in', $noncontentitemtypes); + } $mform->addElement('hidden', 'urlmatchedtypeid', '', array( 'id' => 'id_urlmatchedtypeid' )); $mform->setType('urlmatchedtypeid', PARAM_INT); @@ -158,13 +194,22 @@ class mod_lti_mod_form extends moodleform_mod { $mform->setAdvanced('resourcekey'); $mform->addHelpButton('resourcekey', 'resourcekey', 'lti'); $mform->disabledIf('resourcekey', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('resourcekey', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('resourcekey', 'typeid', 'in', $noncontentitemtypes); + } $mform->setForceLtr('resourcekey'); $mform->addElement('passwordunmask', 'password', get_string('password', 'lti')); $mform->setType('password', PARAM_TEXT); $mform->setAdvanced('password'); $mform->addHelpButton('password', 'password', 'lti'); - $mform->disabledIf('password', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('password', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('password', 'typeid', 'in', $noncontentitemtypes); + } $mform->addElement('textarea', 'instructorcustomparameters', get_string('custom', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('instructorcustomparameters', PARAM_TEXT); @@ -176,13 +221,21 @@ class mod_lti_mod_form extends moodleform_mod { $mform->setType('icon', PARAM_URL); $mform->setAdvanced('icon'); $mform->addHelpButton('icon', 'icon_url', 'lti'); - $mform->disabledIf('icon', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('icon', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('icon', 'typeid', 'in', $noncontentitemtypes); + } $mform->addElement('text', 'secureicon', get_string('secure_icon_url', 'lti'), array('size' => '64')); $mform->setType('secureicon', PARAM_URL); $mform->setAdvanced('secureicon'); $mform->addHelpButton('secureicon', 'secure_icon_url', 'lti'); - $mform->disabledIf('secureicon', 'typeid', 'neq', '0'); + if ($update) { + $mform->disabledIf('secureicon', 'typeid', 'neq', 0); + } else { + $mform->disabledIf('secureicon', 'typeid', 'in', $noncontentitemtypes); + } // Add privacy preferences fieldset where users choose whether to send their data. $mform->addElement('header', 'privacy', get_string('privacy', 'lti')); diff --git a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php index 71050f4f413..79f59e08be9 100644 --- a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php +++ b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php @@ -149,8 +149,13 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base { // Extract all launchable tools from the resource handlers. if ($ok) { $resources = $toolproxyjson->tool_profile->resource_handler; + $messagetypes = [ + 'basic-lti-launch-request', + 'ContentItemSelectionRequest', + ]; foreach ($resources as $resource) { - $found = false; + $launchable = false; + $messages = array(); $tool = new \stdClass(); $iconinfo = null; @@ -164,19 +169,16 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base { } foreach ($resource->message as $message) { - if ($message->message_type == 'basic-lti-launch-request') { - $found = true; - $tool->path = $message->path; - $tool->enabled_capability = $message->enabled_capability; - $tool->parameter = $message->parameter; - break; + if (in_array($message->message_type, $messagetypes)) { + $launchable = $launchable || ($message->message_type === 'basic-lti-launch-request'); + $messages[$message->message_type] = $message; } } - if (!$found) { + if (!$launchable) { continue; } - $tool->name = $resource->resource_name->default_value; + $tool->messages = $messages; $tools[] = $tool; } $ok = count($tools) > 0; @@ -196,17 +198,30 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base { $securebaseurl = $toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url; } foreach ($tools as $tool) { + $messages = $tool->messages; + $launchrequest = $messages['basic-lti-launch-request']; $config = new \stdClass(); - $config->lti_toolurl = "{$baseurl}{$tool->path}"; + $config->lti_toolurl = "{$baseurl}{$launchrequest->path}"; $config->lti_typename = $tool->name; $config->lti_coursevisible = 1; $config->lti_forcessl = 0; + if (isset($messages['ContentItemSelectionRequest'])) { + $contentitemrequest = $messages['ContentItemSelectionRequest']; + $config->lti_contentitem = 1; + if ($launchrequest->path !== $contentitemrequest->path) { + $config->lti_toolurl_ContentItemSelectionRequest = $baseurl . $contentitemrequest->path; + } + $contentitemcapabilities = implode("\n", $contentitemrequest->enabled_capability); + $config->lti_enabledcapability_ContentItemSelectionRequest = $contentitemcapabilities; + $contentitemparams = self::lti_extract_parameters($contentitemrequest->parameter); + $config->lti_parameter_ContentItemSelectionRequest = $contentitemparams; + } $type = new \stdClass(); $type->state = LTI_TOOL_STATE_PENDING; $type->toolproxyid = $toolproxy->id; - $type->enabledcapability = implode("\n", $tool->enabled_capability); - $type->parameter = self::lti_extract_parameters($tool->parameter); + $type->enabledcapability = implode("\n", $launchrequest->enabled_capability); + $type->parameter = self::lti_extract_parameters($launchrequest->parameter); if (!empty($tool->iconpath)) { $type->icon = "{$baseurl}{$tool->iconpath}"; diff --git a/mod/lti/templates/contentitem.mustache b/mod/lti/templates/contentitem.mustache new file mode 100644 index 00000000000..aa317342705 --- /dev/null +++ b/mod/lti/templates/contentitem.mustache @@ -0,0 +1,88 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_lti/contentitem + + Provides a template for the creation of a new external tool instance via the content-item message. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * url The URL the iframe has to load. + * postData The JSON object that contains the information to be POSTed for the ContentItemSelectionRequest. + + Example context (json): + { + "url": "/", + "postData": { + "id": "1", + "course": "1", + "title": "Sample title", + "text": "This is a description" + } + } + +}} +
+
+ {{> mod_lti/loader }} +

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

+ +
+
+ +
+ + + + +
+
+
+{{#js}} + require(['jquery'], function($) { + var loadingContainer = $('.contentitem-loading-container'); + var iframe = $('#contentitem-page-iframe'); + var timeout = setTimeout(function () { + var failedContainer = $('#tool-loading-failed'); + failedContainer.removeClass('hidden'); + }, 20000); + + // Submit form. + $('#contentitem-request-form').submit(); + + iframe.on('load', function() { + loadingContainer.addClass('hidden'); + iframe.removeClass('hidden'); + + // Adjust iframe's width to the fit the container's width. + var containerWidth = $('div.contentitem-container').width(); + $('#contentitem-page-iframe').attr('width', containerWidth); + + var dialogueContentDiv = $('div.contentitem-container').parent(); + var dialogueContainer = dialogueContentDiv.parent(); + // Adjust iframe's height to container's height - 55px (dialogue title bar + top/bottom margins). + var containerHeight = dialogueContainer.height() - 55; + $('#contentitem-page-iframe').attr('height', containerHeight); + }); + }); +{{/js}} diff --git a/mod/lti/tests/behat/addtool.feature b/mod/lti/tests/behat/addtool.feature index 8e51ddd95ba..0703fe66b1b 100644 --- a/mod/lti/tests/behat/addtool.feature +++ b/mod/lti/tests/behat/addtool.feature @@ -30,9 +30,15 @@ Feature: Add tools When I log in as "teacher1" And I follow "Course 1" And I turn editing mode on - And I add a "Teaching Tool 1" to section "1" and I fill the form with: - | Activity name | Test tool activity 1 | - | Launch container | Embed | + And I add a "Teaching Tool 1" to section "1" + # For tool that does not support Content-Item message type, the Select content button must be disabled. + And the "Select content" "button" should be disabled + And I set the field "Activity name" to "Test tool activity 1" + And I click on "Show more..." "link" + And I set the field "Launch container" to "Embed" + And I press "Save and return to course" And I open "Test tool activity 1" actions menu And I choose "Edit settings" in the open action menu Then the field "Preconfigured tool" matches value "Teaching Tool 1" + # When editing settings, the Select content button should be disabled. + And the "Select content" "button" should be disabled diff --git a/mod/lti/tests/behat/contentitem.feature b/mod/lti/tests/behat/contentitem.feature new file mode 100644 index 00000000000..a0d50fe96ec --- /dev/null +++ b/mod/lti/tests/behat/contentitem.feature @@ -0,0 +1,65 @@ +@mod @mod_lti @mod_lti_contentitem +Feature: Content-Item support + In order to easily add activities and content in a course from an external tool + As a teacher + I need to utilise a tool that supports the Content-Item Message type + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I log in as "admin" + And I navigate to "Manage tools" node in "Site administration > Plugins > Activity modules > External tool" + And I follow "configure a tool manually" + And I set the field "Tool name" to "Teaching Tool 1" + And I set the field "Tool base URL/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.php" + And I set the field "Tool configuration usage" to "Show in activity chooser and as a preconfigured tool" + And I expand all fieldsets + And I set the field "Content-Item Message" to "1" + And I press "Save changes" + And I log out + + @javascript + Scenario: Tool that supports Content-Item Message type should be able to configure a tool via the Select content button + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "Teaching Tool 1" to section "1" + Then the "Select content" "button" should be enabled + + @javascript + Scenario: Adding a preconfigured tool that does not support Content-Item. + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "Teaching Tool 1" to section "1" + And the "Select content" "button" should be enabled + And I set the field "Activity name" to "Test tool activity 1" + And I expand all fieldsets + And I set the field "Launch container" to "Embed" + And I press "Save and return to course" + And I open "Test tool activity 1" actions menu + And I follow "Edit settings" in the open menu + Then the field "Preconfigured tool" matches value "Teaching Tool 1" + # When editing settings, the Select content button should be disabled. + And the "Select content" "button" should be disabled + + @javascript + Scenario: Selecting a preconfigured tool that supports Content-Item + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "External tool" to section "1" + And the field "Preconfigured tool" matches value "Automatic, based on launch URL" + And the "Select content" "button" should be disabled + And I set the field "Activity name" to "Test tool activity 1" + And I set the field "Preconfigured tool" to "Teaching Tool 1" + Then the "Select content" "button" should be enabled + And I set the field "Preconfigured tool" to "Automatic, based on launch URL" + And the "Select content" "button" should be disabled diff --git a/mod/lti/tests/behat/contentitemregistration.feature b/mod/lti/tests/behat/contentitemregistration.feature new file mode 100644 index 00000000000..af3cb38625c --- /dev/null +++ b/mod/lti/tests/behat/contentitemregistration.feature @@ -0,0 +1,32 @@ +@mod @mod_lti @mod_lti_contentitem +Feature: Content-Item support + In order to provide external tools that support the Content-Item Message type for teachers and learners + As an admin + I need to be able to configure external tool registrations that support the Content-Item Message type. + + Background: + Given I log in as "admin" + And I navigate to "Manage tools" node in "Site administration > Plugins > Activity modules > External tool" + + Scenario: Verifying ContentItemSelectionRequest selection support in external tool registration + When I follow "Manage external tool registrations" + And I follow "Configure a new external tool registration" + Then I should see "ContentItemSelectionRequest" in the "Capabilities" "select" + + @javascript + Scenario: Creating and editing tool configuration that has Content-Item support + When I follow "configure a tool manually" + And I set the field "Tool name" to "Test tool" + And I set the field "Tool base URL/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.php" + And I set the field "Tool configuration usage" to "Show in activity chooser and as a preconfigured tool" + And I expand all fieldsets + And I set the field "Content-Item Message" to "1" + And I press "Save changes" + And I follow "Edit" + And I expand all fieldsets + Then the field "Content-Item Message" matches value "1" + And I set the field "Content-Item Message" to "0" + And I press "Save changes" + And I follow "Edit" + And I expand all fieldsets + And the field "Content-Item Message" matches value "0" diff --git a/mod/lti/tests/locallib_test.php b/mod/lti/tests/locallib_test.php index 399b429f713..07592fa804e 100644 --- a/mod/lti/tests/locallib_test.php +++ b/mod/lti/tests/locallib_test.php @@ -325,4 +325,161 @@ class mod_lti_locallib_testcase extends advanced_testcase { $this->assertEquals('http://download.moodle.org/unittest/test.jpg', $lti->icon); $this->assertEquals('https://download.moodle.org/unittest/test.jpg', $lti->secureicon); } + + /** + * Tests for lti_build_content_item_selection_request(). + */ + public function test_lti_build_content_item_selection_request() { + $this->resetAfterTest(); + + $this->setAdminUser(); + // Create a tool proxy. + $proxy = mod_lti_external::create_tool_proxy('Test proxy', $this->getExternalTestFileUrl('/test.html'), array(), array()); + + // Create a tool type, associated with that proxy. + $type = new stdClass(); + $data = new stdClass(); + $data->lti_contentitem = true; + $type->state = LTI_TOOL_STATE_CONFIGURED; + $type->name = "Test tool"; + $type->description = "Example description"; + $type->toolproxyid = $proxy->id; + $type->baseurl = $this->getExternalTestFileUrl('/test.html'); + + $typeid = lti_add_type($type, $data); + + $typeconfig = lti_get_type_config($typeid); + + $course = $this->getDataGenerator()->create_course(); + $returnurl = new moodle_url('/'); + + // Default parameters. + $result = lti_build_content_item_selection_request($typeid, $course, $returnurl); + $this->assertNotEmpty($result); + $this->assertNotEmpty($result->params); + $this->assertNotEmpty($result->url); + $params = $result->params; + $url = $result->url; + $this->assertEquals($typeconfig['toolurl'], $url); + $this->assertEquals('ContentItemSelectionRequest', $params['lti_message_type']); + $this->assertEquals(LTI_VERSION_2, $params['lti_version']); + $this->assertEquals('application/vnd.ims.lti.v1.ltilink', $params['accept_media_types']); + $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']); + $this->assertEquals($returnurl->out(false), $params['content_item_return_url']); + $this->assertEquals('false', $params['accept_unsigned']); + $this->assertEquals('false', $params['accept_multiple']); + $this->assertEquals('false', $params['accept_copy_advice']); + $this->assertEquals('false', $params['auto_create']); + $this->assertEquals($type->name, $params['title']); + $this->assertFalse(isset($params['resource_link_id'])); + $this->assertFalse(isset($params['resource_link_title'])); + $this->assertFalse(isset($params['resource_link_description'])); + $this->assertFalse(isset($params['launch_presentation_return_url'])); + $this->assertFalse(isset($params['lis_result_sourcedid'])); + + // Custom parameters. + $title = 'My custom title'; + $text = 'This is the tool description'; + $mediatypes = ['image/*', 'video/*']; + $targets = ['embed', 'iframe']; + $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets, + true, true, true, true, true); + $this->assertNotEmpty($result); + $this->assertNotEmpty($result->params); + $this->assertNotEmpty($result->url); + $params = $result->params; + $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']); + $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']); + $this->assertEquals('true', $params['accept_unsigned']); + $this->assertEquals('true', $params['accept_multiple']); + $this->assertEquals('true', $params['accept_copy_advice']); + $this->assertEquals('true', $params['auto_create']); + $this->assertEquals($title, $params['title']); + $this->assertEquals($text, $params['text']); + + // Invalid flag values. + $result = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text, $mediatypes, $targets, + 'aa', -1, 0, 1, 0xabc); + $this->assertNotEmpty($result); + $this->assertNotEmpty($result->params); + $this->assertNotEmpty($result->url); + $params = $result->params; + $this->assertEquals(implode(',', $mediatypes), $params['accept_media_types']); + $this->assertEquals(implode(',', $targets), $params['accept_presentation_document_targets']); + $this->assertEquals('false', $params['accept_unsigned']); + $this->assertEquals('false', $params['accept_multiple']); + $this->assertEquals('false', $params['accept_copy_advice']); + $this->assertEquals('false', $params['auto_create']); + $this->assertEquals($title, $params['title']); + $this->assertEquals($text, $params['text']); + } + + /** + * Test for lti_build_content_item_selection_request() with nonexistent tool type ID parameter. + */ + public function test_lti_build_content_item_selection_request_invalid_tooltype() { + $this->resetAfterTest(); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $returnurl = new moodle_url('/'); + + // Should throw Exception on non-existent tool type. + $this->expectException('moodle_exception'); + lti_build_content_item_selection_request(1, $course, $returnurl); + } + + /** + * Test for lti_build_content_item_selection_request() with invalid media types parameter. + */ + public function test_lti_build_content_item_selection_request_invalid_mediatypes() { + $this->resetAfterTest(); + + $this->setAdminUser(); + + // Create a tool type, associated with that proxy. + $type = new stdClass(); + $data = new stdClass(); + $data->lti_contentitem = true; + $type->state = LTI_TOOL_STATE_CONFIGURED; + $type->name = "Test tool"; + $type->description = "Example description"; + $type->baseurl = $this->getExternalTestFileUrl('/test.html'); + + $typeid = lti_add_type($type, $data); + $course = $this->getDataGenerator()->create_course(); + $returnurl = new moodle_url('/'); + + // Should throw coding_exception on non-array media types. + $mediatypes = 'image/*,video/*'; + $this->expectException('coding_exception'); + lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', $mediatypes); + } + + /** + * Test for lti_build_content_item_selection_request() with invalid presentation targets parameter. + */ + public function test_lti_build_content_item_selection_request_invalid_presentationtargets() { + $this->resetAfterTest(); + + $this->setAdminUser(); + + // Create a tool type, associated with that proxy. + $type = new stdClass(); + $data = new stdClass(); + $data->lti_contentitem = true; + $type->state = LTI_TOOL_STATE_CONFIGURED; + $type->name = "Test tool"; + $type->description = "Example description"; + $type->baseurl = $this->getExternalTestFileUrl('/test.html'); + + $typeid = lti_add_type($type, $data); + $course = $this->getDataGenerator()->create_course(); + $returnurl = new moodle_url('/'); + + // Should throw coding_exception on non-array presentation targets. + $targets = 'frame,iframe'; + $this->expectException('coding_exception'); + lti_build_content_item_selection_request($typeid, $course, $returnurl, '', '', [], $targets); + } } diff --git a/mod/lti/version.php b/mod/lti/version.php index ce66b5156dd..5549b1f5975 100644 --- a/mod/lti/version.php +++ b/mod/lti/version.php @@ -48,7 +48,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2016052300; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2016052301; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2016051900; // Requires this Moodle version. $plugin->component = 'mod_lti'; // Full name of the plugin (used for diagnostics). $plugin->cron = 0;