From d8f9109a12467fcd549290f4fc3b968f9c7e72cb Mon Sep 17 00:00:00 2001 From: spvickers Date: Sat, 21 Mar 2015 01:10:38 +1100 Subject: [PATCH 1/4] MDL-49609 mod_lti: Add Content-Item message Added option for tools to support Content-Item message and redirect to/from tool provider when creating an instance. --- mod/lti/contentitem.php | 89 +++++++ mod/lti/contentitem2.php | 196 +++++++++++++++ mod/lti/contentitem_return.php | 226 ++++++++++++++++++ mod/lti/edit_form.php | 9 + mod/lti/lang/en/lti.php | 4 + mod/lti/locallib.php | 64 +++-- mod/lti/mod_form.js | 100 ++++++++ mod/lti/mod_form.php | 24 +- .../classes/local/resource/toolproxy.php | 33 ++- mod/lti/styles.css | 18 ++ 10 files changed, 729 insertions(+), 34 deletions(-) create mode 100644 mod/lti/contentitem.php create mode 100644 mod/lti/contentitem2.php create mode 100644 mod/lti/contentitem_return.php diff --git a/mod/lti/contentitem.php b/mod/lti/contentitem.php new file mode 100644 index 0000000000000..d3f549b2b16e6 --- /dev/null +++ b/mod/lti/contentitem.php @@ -0,0 +1,89 @@ +. + +/** + * Display a page containing an iframe for the content-item selection process. + * + * @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'); + +$courseid = required_param('course', PARAM_INT); +$sectionid = required_param('section', PARAM_INT); +$id = required_param('id', PARAM_INT); +$sectionreturn = required_param('sr', PARAM_INT); + +$title = optional_param('title', null, PARAM_TEXT); + +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + +require_login($course); + +$url = new moodle_url('/mod/lti/contentitem.php', array('course' => $courseid)); + +$contentitem = new moodle_url('/mod/lti/contentitem2.php', + array('course' => $courseid, 'section' => $sectionid, 'id' => $id, 'sr' => $sectionreturn, 'title' => $title)); + +echo "

\n"; +echo get_string('register_warning', 'lti'); +echo "\n

\n"; + +echo ''; + +// Output script to make the object tag be as large as possible. +$resize = ' + +'; + +echo $resize; diff --git a/mod/lti/contentitem2.php b/mod/lti/contentitem2.php new file mode 100644 index 0000000000000..3e974370bfc6e --- /dev/null +++ b/mod/lti/contentitem2.php @@ -0,0 +1,196 @@ +. + +/** + * 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'); + +$courseid = required_param('course', PARAM_INT); +$sectionid = required_param('section', PARAM_INT); +$id = required_param('id', PARAM_INT); +$sectionreturn = required_param('sr', PARAM_INT); + +require_login($PAGE->course); + +$tool = lti_get_type($id); +$typeconfig = lti_get_type_config($id); +if (isset($tool->toolproxyid)) { + $toolproxy = lti_get_tool_proxy($tool->toolproxyid); + $key = $toolproxy->guid; + $secret = $toolproxy->secret; +} else { + $toolproxy = null; + if (!empty($instance->resourcekey)) { + $key = $instance->resourcekey; + } else if (!empty($typeconfig['resourcekey'])) { + $key = $typeconfig['resourcekey']; + } else { + $key = ''; + } + if (!empty($instance->password)) { + $secret = $instance->password; + } else if (!empty($typeconfig['password'])) { + $secret = $typeconfig['password']; + } else { + $secret = ''; + } +} +$tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest']; +$tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest']; + +$title = optional_param('title', $tool->name, PARAM_TEXT); + +if (isset($typeconfig['toolurl_ContentItemSelectionRequest'])) { + $endpoint = $typeconfig['toolurl_ContentItemSelectionRequest']; +} else { + $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; +} +$endpoint = trim($endpoint); + +// If the current request is using SSL and a secure tool URL is specified, use it. +if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) { + $endpoint = trim($instance->securetoolurl); +} + +// If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL. +if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { + if (!empty($instance->securetoolurl)) { + $endpoint = trim($instance->securetoolurl); + } + + $endpoint = lti_ensure_url_is_https($endpoint); +} else { + if (!strstr($endpoint, '://')) { + $endpoint = 'http://' . $endpoint; + } +} + +$orgid = (isset($typeconfig['organizationid'])) ? $typeconfig['organizationid'] : ''; + +$course = $PAGE->course; +$islti2 = isset($tool->toolproxyid); +$instance = new stdClass(); +$instance->course = $courseid; +$allparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2); +if ($islti2) { + $requestparams = lti_build_request_lti2($tool, $allparams); +} else { + $requestparams = $allparams; +} +$requestparams = array_merge($requestparams, lti_build_standard_request(null, $orgid, $islti2)); +$customstr = ''; +if (isset($typeconfig['customparameters'])) { + $customstr = $typeconfig['customparameters']; +} +$requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, + '', $islti2)); + +// Allow request params to be updated by sub-plugins. +$plugins = core_component::get_plugin_list('ltisource'); +foreach (array_keys($plugins) as $plugin) { + $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch', + array($instance, $endpoint, $requestparams), array()); + + if (!empty($pluginparams) && is_array($pluginparams)) { + $requestparams = array_merge($requestparams, $pluginparams); + } +} + +if ($islti2) { + $requestparams['lti_version'] = 'LTI-2p0'; +} else { + $requestparams['lti_version'] = 'LTI-1p0'; +} +$requestparams['lti_message_type'] = 'ContentItemSelectionRequest'; + +$requestparams['accept_media_types'] = 'application/vnd.ims.lti.v1.ltilink'; +$requestparams['accept_presentation_document_targets'] = 'frame,iframe,window'; +$requestparams['accept_unsigned'] = 'false'; +$requestparams['accept_multiple'] = 'false'; +$requestparams['auto_create'] = 'true'; +$requestparams['can_confirm'] = 'false'; +$requestparams['accept_copy_advice'] = 'false'; +$requestparams['title'] = $title; + +$returnurlparams = array('course' => $courseid, + 'section' => $sectionid, + 'id' => $id, + 'sr' => $sectionreturn, + 'sesskey' => sesskey()); + +// Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. +$url = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams); +$returnurl = $url->out(false); + +if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { + $returnurl = lti_ensure_url_is_https($returnurl); +} + +$requestparams['content_item_return_url'] = $returnurl; + + +$parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret); + +$endpointurl = new \moodle_url($endpoint); +$endpointparams = $endpointurl->params(); + +// Strip querystring params in endpoint url from $parms to avoid duplication. +if (!empty($endpointparams) && !empty($parms)) { + foreach (array_keys($endpointparams) as $paramname) { + if (isset($parms[$paramname])) { + unset($parms[$paramname]); + } + } +} + +echo "

\n"; +echo get_string('register_warning', 'lti'); +echo "\n

\n"; + +$loading = $OUTPUT->render(new \pix_icon('i/loading', '', 'moodle', + array('style' => 'margin:auto;vertical-align:middle;margin-top:125px;', + 'opacity' => '0.5'))); + +echo "

\n"; +echo $loading; +echo "\n

\n"; + +$script = ' + +'; + +echo $script; + +$content = lti_post_launch_html($parms, $endpoint, false); + +echo $content; diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php new file mode 100644 index 0000000000000..c30485b21a73c --- /dev/null +++ b/mod/lti/contentitem_return.php @@ -0,0 +1,226 @@ +. + +/** + * 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 . '/course/modlib.php'); +require_once($CFG->dirroot . '/mod/lti/lib.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); +require_once($CFG->dirroot . '/mod/lti/OAuth.php'); +require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); + +use moodle\mod\lti as lti; + +$courseid = required_param('course', PARAM_INT); +$sectionid = required_param('section', PARAM_INT); +$id = required_param('id', PARAM_INT); +$sectionreturn = required_param('sr', PARAM_INT); +$messagetype = required_param('lti_message_type', PARAM_TEXT); +$version = required_param('lti_version', PARAM_TEXT); +$consumer_key = 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); +$module = $DB->get_record('modules', array('name' => 'lti'), '*', MUST_EXIST); +$tool = lti_get_type($id); +$typeconfig = lti_get_type_config($id); + +require_login($course); +require_sesskey(); + +if (isset($tool->toolproxyid)) { + $toolproxy = lti_get_tool_proxy($tool->toolproxyid); + $key = $toolproxy->guid; + $secret = $toolproxy->secret; +} else { + $toolproxy = null; + if (!empty($instance->resourcekey)) { + $key = $instance->resourcekey; + } else if (!empty($typeconfig['resourcekey'])) { + $key = $typeconfig['resourcekey']; + } else { + $key = ''; + } + if (!empty($instance->password)) { + $secret = $instance->password; + } else if (!empty($typeconfig['password'])) { + $secret = $typeconfig['password']; + } else { + $secret = ''; + } +} + +if ($consumer_key !== $key) { + throw new Exception('Consumer key is incorrect.'); +} + +$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 (\Exception $e) { + $message = $e->getMessage(); + debugging($e->getMessage() . "\n"); + throw new lti\OAuthException("OAuth signature failed: " . $message); +} + +if ($items) { + $items = json_decode($items); + if ($items->{'@context'} !== 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem') { + throw new Exception('Invalid media type.'); + } + if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) { + throw new Exception('Invalid format.'); + } +} + +$continueurl = course_get_url($course, $sectionid, array('sr' => $sectionreturn)); +if (count($items->{'@graph'}) > 0) { + foreach ($items->{'@graph'} as $item) { + $moduleinfo = new stdClass(); + $moduleinfo->modulename = 'lti'; + $moduleinfo->name = ''; + if (isset($item->title)) { + $moduleinfo->name = $item->title; + } + if (empty($moduleinfo->name)) { + $moduleinfo->name = $tool->name; + } + $moduleinfo->module = $module->id; + $moduleinfo->section = $sectionid; + $moduleinfo->visible = 1; + if (isset($item->url)) { + $moduleinfo->toolurl = $item->url; + $moduleinfo->typeid = 0; + } else { + $moduleinfo->typeid = $id; + } + $moduleinfo->instructorchoicesendname = LTI_SETTING_NEVER; + $moduleinfo->instructorchoicesendemailaddr = LTI_SETTING_NEVER; + $moduleinfo->instructorchoiceacceptgrades = LTI_SETTING_NEVER; + $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; + if (isset($item->placementAdvice->presentationDocumentTarget)) { + if ($item->placementAdvice->presentationDocumentTarget === 'window') { + $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW; + } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') { + $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; + } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') { + $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED; + } + } + if (isset($item->custom)) { + $moduleinfo->instructorcustomparameters = ''; + $first = true; + foreach ($item->custom as $key => $value) { + if (!$first) { + $moduleinfo->instructorcustomparameters .= "\n"; + } + $moduleinfo->instructorcustomparameters .= "{$key}={$value}"; + $first = false; + } + } + $moduleinfo = add_moduleinfo($moduleinfo, $course, null); + } + $clickhere = get_string('click_to_continue', 'lti', (object)array('link' => $continueurl->out())); +} else { + $clickhere = get_string('return_to_course', 'lti', (object)array('link' => $continueurl->out())); +} + +if (!empty($errormsg) || !empty($msg)) { + + $url = new moodle_url('/mod/lti/contentitem_return.php', + array('course' => $courseid)); + $PAGE->set_url($url); + + $pagetitle = strip_tags($course->shortname); + $PAGE->set_title($pagetitle); + $PAGE->set_heading($course->fullname); + + $PAGE->set_pagelayout('embedded'); + + echo $OUTPUT->header(); + + if (!empty($lti) and !empty($context)) { + echo $OUTPUT->heading(format_string($lti->name, true, array('context' => $context))); + } + + if (!empty($errormsg)) { + + echo '

'; + echo get_string('lti_launch_error', 'lti') . ' '; + p($errormsg); + echo "

\n"; + + } + + if (!empty($msg)) { + + echo '

'; + p($msg); + echo "

\n"; + + } + + echo "

{$clickhere}

"; + + echo $OUTPUT->footer(); + +} else { + + $url = $continueurl->out(); + + echo ''; + + $script = " + + "; + + $noscript = " + + "; + + echo $script; + echo $noscript; + + echo ''; + +} diff --git a/mod/lti/edit_form.php b/mod/lti/edit_form.php index 754608daa9cbb..23a22c98a427d 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -101,6 +101,7 @@ public function definition() { $mform->addElement('textarea', 'lti_customparameters', get_string('custom', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('lti_customparameters', PARAM_TEXT); $mform->addHelpButton('lti_customparameters', 'custom', 'lti'); + $mform->setAdvanced('lti_customparameters'); if (!empty($this->_customdata->isadmin)) { $options = array( @@ -136,6 +137,14 @@ public function definition() { $mform->setDefault('lti_launchcontainer', LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS); $mform->addHelpButton('lti_launchcontainer', 'default_launch_container', 'lti'); $mform->setType('lti_launchcontainer', PARAM_INT); + $mform->setAdvanced('lti_launchcontainer'); + + $mform->addElement('checkbox', '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); diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 86d64c6ec1184..ec533ff53932a 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -107,6 +107,10 @@ $string['confirmtoolactivation'] = 'Are you sure you would like to activate this tool?'; $string['courseactivitiesorresources'] = 'Course activities or resources'; $string['course_tool_types'] = 'Course tools'; +$string['configure_item'] = 'Configure item'; +$string['contentitem'] = 'Tool supports Content-Item message'; +$string['contentitem_help'] = 'If selected, the tool provider will participate in the creation of new links added to courses.'; +$string['course_tool_types'] = 'Course tool types'; $string['courseid'] = 'Course ID number'; $string['courseinformation'] = 'Course information'; $string['courselink'] = 'Go to course'; diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index ddec86456c432..832470dc3a064 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -376,18 +376,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 +384,29 @@ 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->servicesalt)) { + $placementsecret = $instance->servicesalt; + if ( isset($placementsecret) && ($islti2 || + $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || + ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && + $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) { + + $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); + $requestparams['lis_result_sourcedid'] = $sourcedid; + } + } if (!empty($instance->id)) { $requestparams['resource_link_id'] = $instance->id; } @@ -407,7 +419,6 @@ 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 || @@ -492,9 +503,11 @@ function lti_build_standard_request($instance, $orgid, $islti2) { $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(); @@ -560,10 +573,14 @@ 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; @@ -1454,6 +1471,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 +1510,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 +2172,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 7318c275b8acf..5095b95e29221 100644 --- a/mod/lti/mod_form.js +++ b/mod/lti/mod_form.js @@ -19,11 +19,26 @@ * @package mod * @subpackage lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) + * @copyright 2015 Vital Source Technologies http://vitalsource.com + * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ (function(){ var Y; + var LTICIP = { + BASE : 'base', + }; + + var CSS = { + PANEL : 'lti-contentitem-panel', + HIDDEN : 'hidden', + WRAP : 'lticip-wrap', + HEADER : 'lticip-header', + CLOSE : 'close', + CONTENT : 'lticip-content', + }; + M.mod_lti = M.mod_lti || {}; M.mod_lti.LTI_SETTING_NEVER = 0; @@ -31,6 +46,13 @@ M.mod_lti.LTI_SETTING_DELEGATE = 2; M.mod_lti.editor = { + _base : null, + _escCloseEvent : null, + _headingNode : null, + _contentNode : null, + _toolList : null, + _onResize : null, + _timer : null, init: function(yui3, settings){ if(yui3){ Y = yui3; @@ -49,6 +71,21 @@ self.updateAutomaticToolMatch(Y.one('#id_securetoolurl')); }; + var create = Y.Node.create; + this._base = create('
') + .append(create('
') + .append(create('
') + .append(create('
')) + .append(create('

'+M.util.get_string('configure_item', 'lti')+'

'))) + .append(create('
' + + '
')) + ); + Y.one('.lti_contentitem').on('change', this.show, this); + this._base.one('.'+CSS.HEADER+' .'+CSS.CLOSE).on('click', this.hide, this); + this._headingNode = this._base.one('.'+CSS.HEADER); + this._contentNode = this._base.one('.'+CSS.CONTENT); + Y.one(document.body).append(this._base); + var typeSelector = Y.one('#id_typeid'); typeSelector.on('change', function(e){ updateToolMatches(); @@ -492,6 +529,69 @@ } } }); + }, + + contentItem: function(el){ + var opt = el.options[el.selectedIndex]; + if(opt.getAttribute('contentitem') == '1') { + window.location.href = opt.getAttribute('contentitemurl') + '&title=' + + encodeURIComponent(document.getElementById('id_name').value); + } + }, + + show : function(e) { + var opt = e.target.get('options').item(e.target.get('selectedIndex')); + if (opt.getAttribute('contentitem') == '1') { + this._toolList = e.target; + e.preventDefault(); + e.halt(); + this._timer = window.setTimeout(this.doReveal, 20000); + this._base.removeClass(CSS.HIDDEN); + var w = this._base.get('winWidth') * 0.8; + var h = this._base.get('winHeight') * 0.8; + var x = (this._base.get('winWidth') - w) / 2; + var y = (this._base.get('winHeight') - h) / 2; + this._base.setStyle('width', '' + w + 'px'); + this._base.setStyle('height', '' + h + 'px'); + this._base.setXY([x,y]); + + var padding = 15; //The bottom of the iframe wasn\'t visible on some themes. Probably because of border widths, etc. + + var viewportHeight = h - parseInt(this._headingNode.getStyle('height')) - padding; + + this._escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this); + + var url = opt.getAttribute('contentitemurl') + '&title=' + + encodeURIComponent(Y.one('#id_name').get('value')); + + var ifr = Y.one('.id_contentitem_if'); + ifr.setAttribute('src', url); + ifr.on('load', this.loaded, this); + ifr.setStyle("height", viewportHeight); + + var frm = Y.one('.mform'); + frm.reset(); + } + }, + + doReveal : function() { + var el = Y.one('#id_warning'); + el.removeClass(CSS.HIDDEN); + }, + + loaded : function() { + window.clearTimeout(this._timer); + }, + + hide : function(e) { + this.loaded(); + if (this._escCloseEvent) { + this._escCloseEvent.detach(); + this._escCloseEvent = null; + } + this._base.addClass(CSS.HIDDEN); + var ifr = Y.one('.id_contentitem_if'); + ifr.setAttribute('src', 'about:blank'); } }; diff --git a/mod/lti/mod_form.php b/mod/lti/mod_form.php index 87819d4e728cf..a75498cc47c72 100644 --- a/mod/lti/mod_form.php +++ b/mod/lti/mod_form.php @@ -43,6 +43,8 @@ * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner + * @copyright 2015 Vital Source Technologies http://vitalsource.com + * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -54,11 +56,12 @@ class mod_lti_mod_form extends moodleform_mod { public function definition() { - global $DB, $PAGE, $OUTPUT, $USER, $COURSE; + global $DB, $PAGE, $OUTPUT, $USER, $COURSE, $sesskey, $section; if ($type = optional_param('type', false, PARAM_ALPHA)) { component_callback("ltisource_$type", 'add_instance_hook'); } + $sectionreturn = optional_param('sr', 0, PARAM_INT); $this->typeid = 0; @@ -95,7 +98,12 @@ public function definition() { $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'); @@ -123,6 +131,15 @@ public function definition() { } else { $attributes = array(); } + if (!$update && $id) { + $config = lti_get_type_config($id); + if (isset($config['contentitem']) && $config['contentitem']) { + $contentitemurl = new moodle_url('/mod/lti/contentitem2.php', + array('course' => $COURSE->id, 'section' => $section, 'id' => $id, 'sr' => $sectionreturn)); + $attributes['contentitem'] = 1; + $attributes['contentitemurl'] = $contentitemurl->out(false); + } + } $tooltypes->addOption($type->name, $id, $attributes); } @@ -247,7 +264,8 @@ public function definition() { array('tooltypedeleted', 'lti'), array('tooltypenotdeleted', 'lti'), array('tooltypeupdated', 'lti'), - array('forced_help', 'lti') + array('forced_help', 'lti'), + array('configure_item', 'lti') ), ); diff --git a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php index 71050f4f413b5..9f146e291dcb4 100644 --- a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php +++ b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php @@ -150,7 +150,8 @@ public function execute($response) { if ($ok) { $resources = $toolproxyjson->tool_profile->resource_handler; foreach ($resources as $resource) { - $found = false; + $launchable = false; + $messages = array(); $tool = new \stdClass(); $iconinfo = null; @@ -164,19 +165,17 @@ public function execute($response) { } 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 (($message->message_type === 'basic-lti-launch-request') || + ($message->message_type === 'ContentItemSelectionRequest')) { + $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 +195,27 @@ public function execute($response) { $securebaseurl = $toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url; } foreach ($tools as $tool) { + $messages = $tool->messages; $config = new \stdClass(); - $config->lti_toolurl = "{$baseurl}{$tool->path}"; + $config->lti_toolurl = "{$baseurl}{$messages['basic-lti-launch-request']->path}"; $config->lti_typename = $tool->name; $config->lti_coursevisible = 1; $config->lti_forcessl = 0; + if (isset($messages['ContentItemSelectionRequest'])) { + $config->lti_contentitem = 1; + if ($messages['basic-lti-launch-request']->path !== $messages['ContentItemSelectionRequest']->path) { + $config->lti_toolurl_ContentItemSelectionRequest = + "{$baseurl}{$messages['ContentItemSelectionRequest']->path}"; + } + $config->lti_enabledcapability_ContentItemSelectionRequest = implode("\n", $messages['ContentItemSelectionRequest']->enabled_capability); + $config->lti_parameter_ContentItemSelectionRequest = self::lti_extract_parameters($messages['ContentItemSelectionRequest']->parameter); + } $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", $messages['basic-lti-launch-request']->enabled_capability); + $type->parameter = self::lti_extract_parameters($messages['basic-lti-launch-request']->parameter); if (!empty($tool->iconpath)) { $type->icon = "{$baseurl}{$tool->iconpath}"; diff --git a/mod/lti/styles.css b/mod/lti/styles.css index 66a23e0f6d5b3..c2e9cbe20c338 100644 --- a/mod/lti/styles.css +++ b/mod/lti/styles.css @@ -380,3 +380,21 @@ border: 1px solid #ddd; border-radius: 4px; } + +/* Styles for mod_form.php to support Content-Item */ +.lti-contentitem-panel {width:400px;background-color:#666;position:absolute;top:10%;left:10%;border:1px solid #666;border-width:0 5px 5px 0;} +.lti-contentitem-panel.hidden {display:none;} +.lti-contentitem-panel .lticip-wrap {margin-top:-5px;margin-left:-5px;background-color:#FFF;border:1px solid #999;height:inherit;} + +.lti-contentitem-panel .lticip-header {background-color:#eee;padding:1px;} +.lti-contentitem-panel .lticip-header h2 {margin:3px 1em 0.5em 1em;font-size:1em;} +.lti-contentitem-panel .lticip-header .close {width:25px;height:15px;position:absolute;top:2px;right:1em;cursor:pointer;background:url("../../../../mod/lti/pix/close.png") no-repeat scroll 0 0 transparent;} + +.lti-contentitem-panel .lticip-content {text-align:center;position:relative;width:100%;border-top:1px solid #999;border-bottom:1px solid #999;} +.lti-contentitem-panel .lticip-ajax-content {overflow:auto;} + +.lti-contentitem-panel .lticip-loading-lightbox {position:absolute;width:100%;height:100%;top:0;left:0;background-color:#FFF;min-width:50px;min-height:50px;} +.lti-contentitem-panel .lticip-loading-lightbox.hidden {display:none;} +.lti-contentitem-panel .lticip-loading-lightbox .loading-icon {margin:auto;vertical-align:middle;margin-top:125px;} + +.dir-rtl .lti-contentitem-panel .lticip-header .close {right: auto;left:1em;} From c1fae2b928a0956cd195679e61bb8b3d0e350553 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Tue, 26 Jul 2016 14:40:50 +0800 Subject: [PATCH 2/4] MDL-49609 mod_lti: Refactor content-item and make it work using JS * Rebase and resolve conflicts from initial patch. * Reorganise contentitem and contentitem_return pages. * Add capability checks for contentitem and contentitem_return pages. * Move the building of Content-Item selection request to a local lib function. * Move contentitem_return processing logic to a local lib function. * Fix type settings update. Content-item checkbox does not get checked/unchecked on load. * Fix tool settings update. Disabled content-item checkbox gets unchecked when tool settings form is submitted. * Add "Select content" button on load which launches the content item selection dialogue. * Move hardcoded HTML and JS to mustache templates and AMD modules. * Use standard YUI dialog for displaying the Content-Item selection page. * Added processing for the following Content-Item properties: - text - icon * On ContentItem selection, fill out form with the configuration data retrieved, instead of automatically saving the tool and redirecting to the course page. * Removed section- and sectionreturn-related code since we're not automatically adding the tool on ContentItem selection. * On mod_lti_mod_form, enable configuration fields if they support ContentItem selection. * New form-field module for setting fields using JS * Change types_config table's 'value' column from char(255) to text --- mod/lti/amd/build/contentitem.min.js | 1 + mod/lti/amd/build/contentitem_return.min.js | 1 + mod/lti/amd/build/form-field.min.js | 1 + mod/lti/amd/src/contentitem.js | 122 ++++++ mod/lti/amd/src/contentitem_return.js | 40 ++ mod/lti/amd/src/form-field.js | 107 +++++ mod/lti/contentitem.php | 77 +--- mod/lti/contentitem2.php | 196 --------- mod/lti/contentitem_return.php | 208 ++-------- mod/lti/db/install.xml | 2 +- mod/lti/db/upgrade.php | 15 + mod/lti/edit_form.php | 18 +- mod/lti/lang/en/lti.php | 15 +- mod/lti/locallib.php | 377 +++++++++++++++++- mod/lti/mod_form.js | 132 ++---- mod/lti/mod_form.php | 68 +++- .../classes/local/resource/toolproxy.php | 26 +- mod/lti/styles.css | 18 - mod/lti/templates/contentitem.mustache | 88 ++++ mod/lti/version.php | 2 +- 20 files changed, 911 insertions(+), 603 deletions(-) create mode 100644 mod/lti/amd/build/contentitem.min.js create mode 100644 mod/lti/amd/build/contentitem_return.min.js create mode 100644 mod/lti/amd/build/form-field.min.js create mode 100644 mod/lti/amd/src/contentitem.js create mode 100644 mod/lti/amd/src/contentitem_return.js create mode 100644 mod/lti/amd/src/form-field.js delete mode 100644 mod/lti/contentitem2.php create mode 100644 mod/lti/templates/contentitem.mustache diff --git a/mod/lti/amd/build/contentitem.min.js b/mod/lti/amd/build/contentitem.min.js new file mode 100644 index 0000000000000..574f74cfa9766 --- /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 0000000000000..6c08d1a888ae4 --- /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 0000000000000..d71dc09b70818 --- /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 0000000000000..cf1fe52633294 --- /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 0000000000000..2ae3ceca6f7e8 --- /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 0000000000000..3d7f445fbef5e --- /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 index d3f549b2b16e6..0b6b87281d55b 100644 --- a/mod/lti/contentitem.php +++ b/mod/lti/contentitem.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Display a page containing an iframe for the content-item selection process. + * 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 @@ -24,66 +24,33 @@ */ require_once('../../config.php'); -require_once($CFG->dirroot.'/mod/lti/lib.php'); -require_once($CFG->dirroot.'/mod/lti/locallib.php'); +require_once($CFG->dirroot . '/mod/lti/lib.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); -$courseid = required_param('course', PARAM_INT); -$sectionid = required_param('section', PARAM_INT); $id = required_param('id', PARAM_INT); -$sectionreturn = required_param('sr', PARAM_INT); - -$title = optional_param('title', null, PARAM_TEXT); +$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); -$url = new moodle_url('/mod/lti/contentitem.php', array('course' => $courseid)); - -$contentitem = new moodle_url('/mod/lti/contentitem2.php', - array('course' => $courseid, 'section' => $sectionid, 'id' => $id, 'sr' => $sectionreturn, 'title' => $title)); - -echo "

\n"; -echo get_string('register_warning', 'lti'); -echo "\n

\n"; - -echo ''; - -// Output script to make the object tag be as large as possible. -$resize = ' - -'; +// Get the launch HTML. +$content = lti_post_launch_html($request->params, $request->url, false); -echo $resize; +echo $content; diff --git a/mod/lti/contentitem2.php b/mod/lti/contentitem2.php deleted file mode 100644 index 3e974370bfc6e..0000000000000 --- a/mod/lti/contentitem2.php +++ /dev/null @@ -1,196 +0,0 @@ -. - -/** - * 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'); - -$courseid = required_param('course', PARAM_INT); -$sectionid = required_param('section', PARAM_INT); -$id = required_param('id', PARAM_INT); -$sectionreturn = required_param('sr', PARAM_INT); - -require_login($PAGE->course); - -$tool = lti_get_type($id); -$typeconfig = lti_get_type_config($id); -if (isset($tool->toolproxyid)) { - $toolproxy = lti_get_tool_proxy($tool->toolproxyid); - $key = $toolproxy->guid; - $secret = $toolproxy->secret; -} else { - $toolproxy = null; - if (!empty($instance->resourcekey)) { - $key = $instance->resourcekey; - } else if (!empty($typeconfig['resourcekey'])) { - $key = $typeconfig['resourcekey']; - } else { - $key = ''; - } - if (!empty($instance->password)) { - $secret = $instance->password; - } else if (!empty($typeconfig['password'])) { - $secret = $typeconfig['password']; - } else { - $secret = ''; - } -} -$tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest']; -$tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest']; - -$title = optional_param('title', $tool->name, PARAM_TEXT); - -if (isset($typeconfig['toolurl_ContentItemSelectionRequest'])) { - $endpoint = $typeconfig['toolurl_ContentItemSelectionRequest']; -} else { - $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; -} -$endpoint = trim($endpoint); - -// If the current request is using SSL and a secure tool URL is specified, use it. -if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) { - $endpoint = trim($instance->securetoolurl); -} - -// If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL. -if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { - if (!empty($instance->securetoolurl)) { - $endpoint = trim($instance->securetoolurl); - } - - $endpoint = lti_ensure_url_is_https($endpoint); -} else { - if (!strstr($endpoint, '://')) { - $endpoint = 'http://' . $endpoint; - } -} - -$orgid = (isset($typeconfig['organizationid'])) ? $typeconfig['organizationid'] : ''; - -$course = $PAGE->course; -$islti2 = isset($tool->toolproxyid); -$instance = new stdClass(); -$instance->course = $courseid; -$allparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2); -if ($islti2) { - $requestparams = lti_build_request_lti2($tool, $allparams); -} else { - $requestparams = $allparams; -} -$requestparams = array_merge($requestparams, lti_build_standard_request(null, $orgid, $islti2)); -$customstr = ''; -if (isset($typeconfig['customparameters'])) { - $customstr = $typeconfig['customparameters']; -} -$requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, - '', $islti2)); - -// Allow request params to be updated by sub-plugins. -$plugins = core_component::get_plugin_list('ltisource'); -foreach (array_keys($plugins) as $plugin) { - $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch', - array($instance, $endpoint, $requestparams), array()); - - if (!empty($pluginparams) && is_array($pluginparams)) { - $requestparams = array_merge($requestparams, $pluginparams); - } -} - -if ($islti2) { - $requestparams['lti_version'] = 'LTI-2p0'; -} else { - $requestparams['lti_version'] = 'LTI-1p0'; -} -$requestparams['lti_message_type'] = 'ContentItemSelectionRequest'; - -$requestparams['accept_media_types'] = 'application/vnd.ims.lti.v1.ltilink'; -$requestparams['accept_presentation_document_targets'] = 'frame,iframe,window'; -$requestparams['accept_unsigned'] = 'false'; -$requestparams['accept_multiple'] = 'false'; -$requestparams['auto_create'] = 'true'; -$requestparams['can_confirm'] = 'false'; -$requestparams['accept_copy_advice'] = 'false'; -$requestparams['title'] = $title; - -$returnurlparams = array('course' => $courseid, - 'section' => $sectionid, - 'id' => $id, - 'sr' => $sectionreturn, - 'sesskey' => sesskey()); - -// Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. -$url = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams); -$returnurl = $url->out(false); - -if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { - $returnurl = lti_ensure_url_is_https($returnurl); -} - -$requestparams['content_item_return_url'] = $returnurl; - - -$parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret); - -$endpointurl = new \moodle_url($endpoint); -$endpointparams = $endpointurl->params(); - -// Strip querystring params in endpoint url from $parms to avoid duplication. -if (!empty($endpointparams) && !empty($parms)) { - foreach (array_keys($endpointparams) as $paramname) { - if (isset($parms[$paramname])) { - unset($parms[$paramname]); - } - } -} - -echo "

\n"; -echo get_string('register_warning', 'lti'); -echo "\n

\n"; - -$loading = $OUTPUT->render(new \pix_icon('i/loading', '', 'moodle', - array('style' => 'margin:auto;vertical-align:middle;margin-top:125px;', - 'opacity' => '0.5'))); - -echo "

\n"; -echo $loading; -echo "\n

\n"; - -$script = ' - -'; - -echo $script; - -$content = lti_post_launch_html($parms, $endpoint, false); - -echo $content; diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php index c30485b21a73c..53cc5de019522 100644 --- a/mod/lti/contentitem_return.php +++ b/mod/lti/contentitem_return.php @@ -24,203 +24,53 @@ */ require_once('../../config.php'); -require_once($CFG->dirroot . '/course/modlib.php'); -require_once($CFG->dirroot . '/mod/lti/lib.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); -require_once($CFG->dirroot . '/mod/lti/OAuth.php'); -require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); -use moodle\mod\lti as lti; - -$courseid = required_param('course', PARAM_INT); -$sectionid = required_param('section', PARAM_INT); $id = required_param('id', PARAM_INT); -$sectionreturn = required_param('sr', PARAM_INT); +$courseid = required_param('course', PARAM_INT); $messagetype = required_param('lti_message_type', PARAM_TEXT); $version = required_param('lti_version', PARAM_TEXT); -$consumer_key = required_param('oauth_consumer_key', PARAM_RAW); - +$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); -$module = $DB->get_record('modules', array('name' => 'lti'), '*', MUST_EXIST); -$tool = lti_get_type($id); -$typeconfig = lti_get_type_config($id); - require_login($course); require_sesskey(); - -if (isset($tool->toolproxyid)) { - $toolproxy = lti_get_tool_proxy($tool->toolproxyid); - $key = $toolproxy->guid; - $secret = $toolproxy->secret; -} else { - $toolproxy = null; - if (!empty($instance->resourcekey)) { - $key = $instance->resourcekey; - } else if (!empty($typeconfig['resourcekey'])) { - $key = $typeconfig['resourcekey']; - } else { - $key = ''; - } - if (!empty($instance->password)) { - $secret = $instance->password; - } else if (!empty($typeconfig['password'])) { - $secret = $typeconfig['password']; - } else { - $secret = ''; - } -} - -if ($consumer_key !== $key) { - throw new Exception('Consumer key is incorrect.'); -} - -$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 (\Exception $e) { - $message = $e->getMessage(); - debugging($e->getMessage() . "\n"); - throw new lti\OAuthException("OAuth signature failed: " . $message); -} - -if ($items) { - $items = json_decode($items); - if ($items->{'@context'} !== 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem') { - throw new Exception('Invalid media type.'); - } - if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) { - throw new Exception('Invalid format.'); - } -} - -$continueurl = course_get_url($course, $sectionid, array('sr' => $sectionreturn)); -if (count($items->{'@graph'}) > 0) { - foreach ($items->{'@graph'} as $item) { - $moduleinfo = new stdClass(); - $moduleinfo->modulename = 'lti'; - $moduleinfo->name = ''; - if (isset($item->title)) { - $moduleinfo->name = $item->title; - } - if (empty($moduleinfo->name)) { - $moduleinfo->name = $tool->name; - } - $moduleinfo->module = $module->id; - $moduleinfo->section = $sectionid; - $moduleinfo->visible = 1; - if (isset($item->url)) { - $moduleinfo->toolurl = $item->url; - $moduleinfo->typeid = 0; - } else { - $moduleinfo->typeid = $id; - } - $moduleinfo->instructorchoicesendname = LTI_SETTING_NEVER; - $moduleinfo->instructorchoicesendemailaddr = LTI_SETTING_NEVER; - $moduleinfo->instructorchoiceacceptgrades = LTI_SETTING_NEVER; - $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; - if (isset($item->placementAdvice->presentationDocumentTarget)) { - if ($item->placementAdvice->presentationDocumentTarget === 'window') { - $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW; - } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') { - $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; - } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') { - $moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED; - } - } - if (isset($item->custom)) { - $moduleinfo->instructorcustomparameters = ''; - $first = true; - foreach ($item->custom as $key => $value) { - if (!$first) { - $moduleinfo->instructorcustomparameters .= "\n"; - } - $moduleinfo->instructorcustomparameters .= "{$key}={$value}"; - $first = false; - } - } - $moduleinfo = add_moduleinfo($moduleinfo, $course, null); +$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(); } - $clickhere = get_string('click_to_continue', 'lti', (object)array('link' => $continueurl->out())); -} else { - $clickhere = get_string('return_to_course', 'lti', (object)array('link' => $continueurl->out())); } -if (!empty($errormsg) || !empty($msg)) { - - $url = new moodle_url('/mod/lti/contentitem_return.php', - array('course' => $courseid)); - $PAGE->set_url($url); - - $pagetitle = strip_tags($course->shortname); - $PAGE->set_title($pagetitle); - $PAGE->set_heading($course->fullname); - - $PAGE->set_pagelayout('embedded'); - - echo $OUTPUT->header(); +$pageurl = new moodle_url('/mod/lti/contentitem_return.php'); +$PAGE->set_url($pageurl); +$PAGE->set_pagelayout('popup'); +echo $OUTPUT->header(); - if (!empty($lti) and !empty($context)) { - echo $OUTPUT->heading(format_string($lti->name, true, array('context' => $context))); - } - - if (!empty($errormsg)) { - - echo '

'; - echo get_string('lti_launch_error', 'lti') . ' '; - p($errormsg); - echo "

\n"; - - } +// 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]); - if (!empty($msg)) { +echo $OUTPUT->footer(); - echo '

'; - p($msg); - echo "

\n"; +// 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'); } - - echo "

{$clickhere}

"; - - echo $OUTPUT->footer(); - -} else { - - $url = $continueurl->out(); - - echo ''; - - $script = " - - "; - - $noscript = " - - "; - - echo $script; - echo $noscript; - - echo ''; - + \core\notification::success($msg); } diff --git a/mod/lti/db/install.xml b/mod/lti/db/install.xml index 2fbbec06f324e..040f32edae3da 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 01d3131097f49..ce19319d830ee 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 23a22c98a427d..bb1849cf93b9d 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -101,7 +101,6 @@ public function definition() { $mform->addElement('textarea', 'lti_customparameters', get_string('custom', 'lti'), array('rows' => 4, 'cols' => 60)); $mform->setType('lti_customparameters', PARAM_TEXT); $mform->addHelpButton('lti_customparameters', 'custom', 'lti'); - $mform->setAdvanced('lti_customparameters'); if (!empty($this->_customdata->isadmin)) { $options = array( @@ -137,9 +136,8 @@ public function definition() { $mform->setDefault('lti_launchcontainer', LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS); $mform->addHelpButton('lti_launchcontainer', 'default_launch_container', 'lti'); $mform->setType('lti_launchcontainer', PARAM_INT); - $mform->setAdvanced('lti_launchcontainer'); - $mform->addElement('checkbox', 'lti_contentitem', ' ', ' ' . get_string('contentitem', 'lti')); + $mform->addElement('advcheckbox', 'lti_contentitem', get_string('contentitem', 'lti')); $mform->addHelpButton('lti_contentitem', 'contentitem', 'lti'); $mform->setAdvanced('lti_contentitem'); if ($istool) { @@ -238,4 +236,18 @@ public function definition() { $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 ec533ff53932a..cc7df13607552 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -105,12 +105,10 @@ $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['configure_item'] = 'Configure item'; -$string['contentitem'] = 'Tool supports Content-Item message'; -$string['contentitem_help'] = 'If selected, the tool provider will participate in the creation of new links added to courses.'; -$string['course_tool_types'] = 'Course tool types'; +$string['courseactivitiesorresources'] = 'Course activities or resources'; $string['courseid'] = 'Course ID number'; $string['courseinformation'] = 'Course information'; $string['courselink'] = 'Go to course'; @@ -169,7 +167,12 @@ $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'; @@ -407,6 +410,7 @@ $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.'; @@ -448,6 +452,7 @@ $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 832470dc3a064..ece6c9f41ab46 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -55,6 +55,8 @@ 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_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. * @@ -396,17 +401,6 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl $intro = str_replace("\n", "\r\n", $intro); $requestparams['resource_link_description'] = $intro; } - if (!empty($instance->servicesalt)) { - $placementsecret = $instance->servicesalt; - if ( isset($placementsecret) && ($islti2 || - $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || - ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && - $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS))) { - - $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); - $requestparams['lis_result_sourcedid'] = $sourcedid; - } - } if (!empty($instance->id)) { $requestparams['resource_link_id'] = $instance->id; } @@ -420,10 +414,11 @@ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $isl $requestparams['lis_course_section_sourcedid'] = $course->idnumber; } - 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; @@ -445,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; @@ -453,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; } @@ -492,13 +491,14 @@ 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(); @@ -525,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; @@ -586,6 +586,343 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus 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 = ''; diff --git a/mod/lti/mod_form.js b/mod/lti/mod_form.js index 5095b95e29221..1044aed3991c2 100644 --- a/mod/lti/mod_form.js +++ b/mod/lti/mod_form.js @@ -19,26 +19,11 @@ * @package mod * @subpackage lti * @copyright Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com) - * @copyright 2015 Vital Source Technologies http://vitalsource.com - * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ (function(){ var Y; - var LTICIP = { - BASE : 'base', - }; - - var CSS = { - PANEL : 'lti-contentitem-panel', - HIDDEN : 'hidden', - WRAP : 'lticip-wrap', - HEADER : 'lticip-header', - CLOSE : 'close', - CONTENT : 'lticip-content', - }; - M.mod_lti = M.mod_lti || {}; M.mod_lti.LTI_SETTING_NEVER = 0; @@ -46,13 +31,6 @@ M.mod_lti.LTI_SETTING_DELEGATE = 2; M.mod_lti.editor = { - _base : null, - _escCloseEvent : null, - _headingNode : null, - _contentNode : null, - _toolList : null, - _onResize : null, - _timer : null, init: function(yui3, settings){ if(yui3){ Y = yui3; @@ -71,22 +49,10 @@ self.updateAutomaticToolMatch(Y.one('#id_securetoolurl')); }; - var create = Y.Node.create; - this._base = create('
') - .append(create('
') - .append(create('
') - .append(create('
')) - .append(create('

'+M.util.get_string('configure_item', 'lti')+'

'))) - .append(create('
' + - '
')) - ); - Y.one('.lti_contentitem').on('change', this.show, this); - this._base.one('.'+CSS.HEADER+' .'+CSS.CLOSE).on('click', this.hide, this); - this._headingNode = this._base.one('.'+CSS.HEADER); - this._contentNode = this._base.one('.'+CSS.CONTENT); - Y.one(document.body).append(this._base); - + var contentItemButton = Y.one('[name="selectcontent"]'); + var contentItemUrl = contentItemButton.getAttribute('data-contentitemurl'); var typeSelector = Y.one('#id_typeid'); + typeSelector.on('change', function(e){ updateToolMatches(); @@ -103,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(); @@ -531,68 +518,17 @@ }); }, - contentItem: function(el){ - var opt = el.options[el.selectedIndex]; - if(opt.getAttribute('contentitem') == '1') { - window.location.href = opt.getAttribute('contentitemurl') + '&title=' + - encodeURIComponent(document.getElementById('id_name').value); - } - }, - - show : function(e) { - var opt = e.target.get('options').item(e.target.get('selectedIndex')); - if (opt.getAttribute('contentitem') == '1') { - this._toolList = e.target; - e.preventDefault(); - e.halt(); - this._timer = window.setTimeout(this.doReveal, 20000); - this._base.removeClass(CSS.HIDDEN); - var w = this._base.get('winWidth') * 0.8; - var h = this._base.get('winHeight') * 0.8; - var x = (this._base.get('winWidth') - w) / 2; - var y = (this._base.get('winHeight') - h) / 2; - this._base.setStyle('width', '' + w + 'px'); - this._base.setStyle('height', '' + h + 'px'); - this._base.setXY([x,y]); - - var padding = 15; //The bottom of the iframe wasn\'t visible on some themes. Probably because of border widths, etc. - - var viewportHeight = h - parseInt(this._headingNode.getStyle('height')) - padding; - - this._escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this); - - var url = opt.getAttribute('contentitemurl') + '&title=' + - encodeURIComponent(Y.one('#id_name').get('value')); - - var ifr = Y.one('.id_contentitem_if'); - ifr.setAttribute('src', url); - ifr.on('load', this.loaded, this); - ifr.setStyle("height", viewportHeight); - - var frm = Y.one('.mform'); - frm.reset(); - } - }, - - doReveal : function() { - var el = Y.one('#id_warning'); - el.removeClass(CSS.HIDDEN); - }, - - loaded : function() { - window.clearTimeout(this._timer); - }, - - hide : function(e) { - this.loaded(); - if (this._escCloseEvent) { - this._escCloseEvent.detach(); - this._escCloseEvent = null; + /** + * 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'); } - this._base.addClass(CSS.HIDDEN); - var ifr = Y.one('.id_contentitem_if'); - ifr.setAttribute('src', 'about:blank'); + return false; } - }; })(); diff --git a/mod/lti/mod_form.php b/mod/lti/mod_form.php index a75498cc47c72..7219faa924dab 100644 --- a/mod/lti/mod_form.php +++ b/mod/lti/mod_form.php @@ -43,8 +43,6 @@ * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner - * @copyright 2015 Vital Source Technologies http://vitalsource.com - * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -56,12 +54,11 @@ class mod_lti_mod_form extends moodleform_mod { public function definition() { - global $DB, $PAGE, $OUTPUT, $USER, $COURSE, $sesskey, $section; + global $PAGE, $OUTPUT, $COURSE; if ($type = optional_param('type', false, PARAM_ALPHA)) { component_callback("ltisource_$type", 'add_instance_hook'); } - $sectionreturn = optional_param('sr', 0, PARAM_INT); $this->typeid = 0; @@ -109,6 +106,9 @@ public function definition() { $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; @@ -133,27 +133,46 @@ public function definition() { } if (!$update && $id) { $config = lti_get_type_config($id); - if (isset($config['contentitem']) && $config['contentitem']) { - $contentitemurl = new moodle_url('/mod/lti/contentitem2.php', - array('course' => $COURSE->id, 'section' => $section, 'id' => $id, 'sr' => $sectionreturn)); - $attributes['contentitem'] = 1; - $attributes['contentitemurl'] = $contentitemurl->out(false); + 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); @@ -174,13 +193,21 @@ public function definition() { $mform->setType('resourcekey', PARAM_TEXT); $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->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); @@ -191,13 +218,21 @@ public function definition() { $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')); @@ -264,8 +299,7 @@ public function definition() { array('tooltypedeleted', 'lti'), array('tooltypenotdeleted', 'lti'), array('tooltypeupdated', 'lti'), - array('forced_help', 'lti'), - array('configure_item', 'lti') + array('forced_help', 'lti') ), ); diff --git a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php index 9f146e291dcb4..79f59e08be927 100644 --- a/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php +++ b/mod/lti/service/toolproxy/classes/local/resource/toolproxy.php @@ -149,6 +149,10 @@ public function execute($response) { // 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) { $launchable = false; $messages = array(); @@ -165,8 +169,7 @@ public function execute($response) { } foreach ($resource->message as $message) { - if (($message->message_type === 'basic-lti-launch-request') || - ($message->message_type === 'ContentItemSelectionRequest')) { + if (in_array($message->message_type, $messagetypes)) { $launchable = $launchable || ($message->message_type === 'basic-lti-launch-request'); $messages[$message->message_type] = $message; } @@ -196,26 +199,29 @@ public function execute($response) { } foreach ($tools as $tool) { $messages = $tool->messages; + $launchrequest = $messages['basic-lti-launch-request']; $config = new \stdClass(); - $config->lti_toolurl = "{$baseurl}{$messages['basic-lti-launch-request']->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 ($messages['basic-lti-launch-request']->path !== $messages['ContentItemSelectionRequest']->path) { - $config->lti_toolurl_ContentItemSelectionRequest = - "{$baseurl}{$messages['ContentItemSelectionRequest']->path}"; + if ($launchrequest->path !== $contentitemrequest->path) { + $config->lti_toolurl_ContentItemSelectionRequest = $baseurl . $contentitemrequest->path; } - $config->lti_enabledcapability_ContentItemSelectionRequest = implode("\n", $messages['ContentItemSelectionRequest']->enabled_capability); - $config->lti_parameter_ContentItemSelectionRequest = self::lti_extract_parameters($messages['ContentItemSelectionRequest']->parameter); + $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", $messages['basic-lti-launch-request']->enabled_capability); - $type->parameter = self::lti_extract_parameters($messages['basic-lti-launch-request']->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/styles.css b/mod/lti/styles.css index c2e9cbe20c338..66a23e0f6d5b3 100644 --- a/mod/lti/styles.css +++ b/mod/lti/styles.css @@ -380,21 +380,3 @@ border: 1px solid #ddd; border-radius: 4px; } - -/* Styles for mod_form.php to support Content-Item */ -.lti-contentitem-panel {width:400px;background-color:#666;position:absolute;top:10%;left:10%;border:1px solid #666;border-width:0 5px 5px 0;} -.lti-contentitem-panel.hidden {display:none;} -.lti-contentitem-panel .lticip-wrap {margin-top:-5px;margin-left:-5px;background-color:#FFF;border:1px solid #999;height:inherit;} - -.lti-contentitem-panel .lticip-header {background-color:#eee;padding:1px;} -.lti-contentitem-panel .lticip-header h2 {margin:3px 1em 0.5em 1em;font-size:1em;} -.lti-contentitem-panel .lticip-header .close {width:25px;height:15px;position:absolute;top:2px;right:1em;cursor:pointer;background:url("../../../../mod/lti/pix/close.png") no-repeat scroll 0 0 transparent;} - -.lti-contentitem-panel .lticip-content {text-align:center;position:relative;width:100%;border-top:1px solid #999;border-bottom:1px solid #999;} -.lti-contentitem-panel .lticip-ajax-content {overflow:auto;} - -.lti-contentitem-panel .lticip-loading-lightbox {position:absolute;width:100%;height:100%;top:0;left:0;background-color:#FFF;min-width:50px;min-height:50px;} -.lti-contentitem-panel .lticip-loading-lightbox.hidden {display:none;} -.lti-contentitem-panel .lticip-loading-lightbox .loading-icon {margin:auto;vertical-align:middle;margin-top:125px;} - -.dir-rtl .lti-contentitem-panel .lticip-header .close {right: auto;left:1em;} diff --git a/mod/lti/templates/contentitem.mustache b/mod/lti/templates/contentitem.mustache new file mode 100644 index 0000000000000..aa3173427056e --- /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/version.php b/mod/lti/version.php index ce66b5156dddf..5549b1f597575 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; From 6696cdab8fab6f736ef54b1148d7592eec52aec8 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Mon, 8 Aug 2016 12:52:10 +0800 Subject: [PATCH 3/4] MDL-49609 mod_lti: Content-Item message type-related Behat tests --- mod/lti/tests/behat/addtool.feature | 12 +++- mod/lti/tests/behat/contentitem.feature | 65 +++++++++++++++++++ .../behat/contentitemregistration.feature | 32 +++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 mod/lti/tests/behat/contentitem.feature create mode 100644 mod/lti/tests/behat/contentitemregistration.feature diff --git a/mod/lti/tests/behat/addtool.feature b/mod/lti/tests/behat/addtool.feature index 27336fca35fc6..73b9fcfb9f45e 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 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 diff --git a/mod/lti/tests/behat/contentitem.feature b/mod/lti/tests/behat/contentitem.feature new file mode 100644 index 0000000000000..a0d50fe96ecac --- /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 0000000000000..af3cb38625c25 --- /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" From 1e7490ad9a20cef8a39c05452d761f3749acba25 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 4 Aug 2016 17:45:04 +0800 Subject: [PATCH 4/4] MDL-49609 mod_lti: Content-Item message type-related unit tests --- mod/lti/tests/locallib_test.php | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/mod/lti/tests/locallib_test.php b/mod/lti/tests/locallib_test.php index 399b429f71391..07592fa804e85 100644 --- a/mod/lti/tests/locallib_test.php +++ b/mod/lti/tests/locallib_test.php @@ -325,4 +325,161 @@ public function test_lti_load_tool_from_cartridge() { $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); + } }