diff --git a/mod/lti/contentitem.php b/mod/lti/contentitem.php new file mode 100644 index 00000000000..d3f549b2b16 --- /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 00000000000..3e974370bfc --- /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 00000000000..c30485b21a7 --- /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 754608daa9c..23a22c98a42 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -101,6 +101,7 @@ class mod_lti_edit_types_form extends moodleform{ $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 @@ class mod_lti_edit_types_form extends moodleform{ $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 86d64c6ec11..ec533ff5393 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -107,6 +107,10 @@ $string['configured'] = 'Configured'; $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 ddec86456c4..832470dc3a0 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 7318c275b8a..5095b95e292 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('