diff --git a/backup/moodle2/restore_subplugin.class.php b/backup/moodle2/restore_subplugin.class.php index 640968b88afbb..c848219ccf867 100644 --- a/backup/moodle2/restore_subplugin.class.php +++ b/backup/moodle2/restore_subplugin.class.php @@ -87,6 +87,23 @@ public function launch_after_execute_methods() { } } + /** + * The after_restore dispatcher for any restore_subplugin class. + * + * This method will dispatch execution to the corresponding + * after_restore_xxx() method when available, with xxx + * being the connection point of the instance, so subplugin + * classes with multiple connection points will support + * multiple after_restore methods, one for each connection point. + */ + public function launch_after_restore_methods() { + // Check if the after_restore method exists and launch it. + $afterestore = 'after_restore_' . basename($this->connectionpoint->get_path()); + if (method_exists($this, $afterestore)) { + $this->$afterestore(); + } + } + // Protected API starts here // restore_step/structure_step/task wrappers diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 302e5363dbf83..cb6c2fe2899fb 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1804,7 +1804,7 @@ public static function standard_plugins_list($type) { ), 'ltiservice' => array( - 'memberships', 'profile', 'toolproxy', 'toolsettings' + 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings' ), 'mlbackend' => array( diff --git a/mod/lti/classes/local/ltiservice/resource_base.php b/mod/lti/classes/local/ltiservice/resource_base.php index 4d617fe1f2120..1fdb72ec76667 100644 --- a/mod/lti/classes/local/ltiservice/resource_base.php +++ b/mod/lti/classes/local/ltiservice/resource_base.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); @@ -41,7 +42,16 @@ */ abstract class resource_base { - /** @var object Service associated with this resource. */ + /** HTTP Post method */ + const HTTP_POST = 'POST'; + /** HTTP Get method */ + const HTTP_GET = 'GET'; + /** HTTP Put method */ + const HTTP_PUT = 'PUT'; + /** HTTP Delete method */ + const HTTP_DELETE = 'DELETE'; + + /** @var service_base Service associated with this resource. */ private $service; /** @var string Type for this resource. */ protected $type; @@ -62,7 +72,7 @@ abstract class resource_base { /** * Class constructor. * - * @param mod_lti\local\ltiservice\service_base $service Service instance + * @param service_base $service Service instance */ public function __construct($service) { @@ -125,7 +135,7 @@ public function get_type() { /** * Get the resource's service. * - * @return mod_lti\local\ltiservice\service_base + * @return mixed */ public function get_service() { @@ -190,7 +200,7 @@ public function get_endpoint() { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param response $response Response object for this request. */ public abstract function execute($response); @@ -224,7 +234,7 @@ public function check_tool_proxy($toolproxyguid, $body = null) { } } if (!$ok) { - debugging('Requested service not included in tool proxy: ' . $this->get_id()); + debugging('Requested service not included in tool proxy: ' . $this->get_id(), DEBUG_DEVELOPER); } } } @@ -233,6 +243,45 @@ public function check_tool_proxy($toolproxyguid, $body = null) { } + /** + * Check to make sure the request is valid. + * + * @param int $typeid The typeid we want to use + * @param int $contextid The course we are at + * @param string $permissionrequested The permission to be checked + * @param string $body Body of HTTP request message + * + * @return boolean + */ + public function check_type($typeid, $contextid, $permissionrequested, $body = null) { + $ok = false; + if ($this->get_service()->check_type($typeid, $contextid, $body)) { + $neededpermissions = $this->get_permissions($typeid); + foreach ($neededpermissions as $permission) { + if ($permission == $permissionrequested) { + $ok = true; + break; + } + } + if (!$ok) { + debugging('Requested service ' . $permissionrequested . ' not included in tool type: ' . $typeid, + DEBUG_DEVELOPER); + } + } + return $ok; + + } + + /** + * get permissions from the config of the tool for that resource + * + * @param int $ltitype Type of LTI + * @return array with the permissions related to this resource by the $ltitype or empty if none. + */ + public function get_permissions($ltitype) { + return array(); + } + /** * Parse a value for custom parameter substitution variables. * diff --git a/mod/lti/classes/local/ltiservice/response.php b/mod/lti/classes/local/ltiservice/response.php index 5029d216acedb..8f4bcabcd2e8e 100644 --- a/mod/lti/classes/local/ltiservice/response.php +++ b/mod/lti/classes/local/ltiservice/response.php @@ -54,6 +54,8 @@ class response { private $body; /** @var array HTTP response codes. */ private $responsecodes; + /** @var array HTTP additional headers. */ + private $additionalheaders; /** * Class constructor. @@ -83,6 +85,7 @@ public function __construct() { 500 => 'Internal Server Error', 501 => 'Not Implemented' ); + $this->additionalheaders = array(); } @@ -202,11 +205,23 @@ public function set_body($body) { $this->body = $body; } + /** + * Add an additional header. + * + * @param string $header The new header + */ + public function add_additional_header($header) { + array_push($this->additionalheaders, $header); + } + /** * Send the response. */ public function send() { header("HTTP/1.0 {$this->code} {$this->get_reason()}"); + foreach ($this->additionalheaders as $header) { + header($header); + } if (($this->code >= 200) && ($this->code < 300)) { if (!empty($this->contenttype)) { header("Content-Type: {$this->contenttype};charset=UTF-8"); diff --git a/mod/lti/classes/local/ltiservice/service_base.php b/mod/lti/classes/local/ltiservice/service_base.php index 73726bd4e224e..26a89c1bcd8a5 100644 --- a/mod/lti/classes/local/ltiservice/service_base.php +++ b/mod/lti/classes/local/ltiservice/service_base.php @@ -28,11 +28,13 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/OAuthBody.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; +use stdClass; /** @@ -133,10 +135,80 @@ public function set_tool_proxy($toolproxy) { /** * Get the resources for this service. * - * @return array + * @return resource_base[] */ abstract public function get_resources(); + /** + * Returns the configuration options for this service. + * + * @param \MoodleQuickForm $mform Moodle quickform object definition + */ + public function get_configuration_options(&$mform) { + + } + + /** + * Return an array with the names of the parameters that the service will be saving in the configuration + * + * @return array Names list of the parameters that the service will be saving in the configuration + */ + public function get_configuration_parameter_names() { + return array(); + } + + /** + * Default implementation will check for the existence of at least one mod_lti entry for that tool and context. + * + * It may be overridden if other inferences can be done. + * + * Ideally a Site Tool should be explicitly engaged with a course, the check on the presence of a link is a proxy + * to infer a Site Tool engagement until an explicit Site Tool - Course relationship exists. + * + * @param int $typeid The tool lti type id. + * @param int $courseid The course id. + * @return bool returns True if tool is used in context, false otherwise. + */ + public function is_used_in_context($typeid, $courseid) { + global $DB; + + $ok = $DB->record_exists('lti', array('course' => $courseid, 'typeid' => $typeid)); + return $ok || $DB->record_exists('lti_types', array('course' => $courseid, 'id' => $typeid)); + } + + /** + * Checks if there is a site tool or a course tool for this site. + * + * @param int $typeid The tool lti type id. + * @param int $courseid The course id. + * @return bool returns True if tool is allowed in context, false otherwise. + */ + public function is_allowed_in_context($typeid, $courseid) { + global $DB; + + // Check if it is a Course tool for this course or a Site tool. + $type = $DB->get_record('lti_types', array('id' => $typeid)); + + return $type && ($type->course == $courseid || $type->course == SITEID); + } + + /** + * Return an array of key/values to add to the launch parameters. + * + * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. + * @param string $courseid The course id. + * @param string $userid The user id. + * @param string $typeid The tool lti type id. + * @param string $modlti The id of the lti activity. + * + * The type is passed to check the configuration and not return parameters for services not used. + * + * @return array Key/value pairs to add as launch parameters. + */ + public function get_launch_parameters($messagetype, $courseid, $userid, $typeid, $modlti = null) { + return array(); + } + /** * Get the path for service requests. * @@ -202,9 +274,35 @@ public function check_tool_proxy($toolproxyguid, $body = null) { if ($ok) { $this->toolproxy = $toolproxy; } - return $ok; + } + /** + * Check that the request has been properly signed. + * + * @param int $typeid The tool id + * @param int $courseid The course we are at + * @param string $body Request body (null if none) + * + * @return bool + */ + public function check_type($typeid, $courseid, $body = null) { + $ok = false; + $tool = null; + $consumerkey = lti\get_oauth_key_from_headers(); + if (empty($typeid)) { + return $ok; + } else if ($this->is_allowed_in_context($typeid, $courseid)) { + $tool = lti_get_type_type_config($typeid); + if ($tool !== false) { + if (!$this->is_unsigned() && ($tool->lti_resourcekey == $consumerkey)) { + $ok = $this->check_signature($tool->lti_resourcekey, $tool->lti_password, $body); + } else { + $ok = $this->is_unsigned(); + } + } + } + return $ok; } /** diff --git a/mod/lti/edit_form.php b/mod/lti/edit_form.php index 8db9ca53c95c7..5e618dec0f2d1 100644 --- a/mod/lti/edit_form.php +++ b/mod/lti/edit_form.php @@ -49,10 +49,24 @@ defined('MOODLE_INTERNAL') || die; +global $CFG; require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot.'/mod/lti/locallib.php'); -class mod_lti_edit_types_form extends moodleform{ +/** + * LTI Edit Form + * + * @package mod_lti + * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis + * marc.alier@upc.edu + * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_lti_edit_types_form extends moodleform { + + /** + * Define this form. + */ public function definition() { global $CFG; @@ -147,6 +161,16 @@ public function definition() { $mform->disabledIf('lti_contentitem', null); } + $mform->addElement('text', 'lti_toolurl_ContentItemSelectionRequest', + get_string('toolurl_contentitemselectionrequest', 'lti'), array('size' => '64')); + $mform->setType('lti_toolurl_ContentItemSelectionRequest', PARAM_URL); + $mform->setAdvanced('lti_toolurl_ContentItemSelectionRequest'); + $mform->addHelpButton('lti_toolurl_ContentItemSelectionRequest', 'toolurl_contentitemselectionrequest', 'lti'); + $mform->disabledIf('lti_toolurl_ContentItemSelectionRequest', 'lti_contentitem', 'notchecked'); + if ($istool) { + $mform->disabledIf('lti_toolurl__ContentItemSelectionRequest', null); + } + $mform->addElement('hidden', 'oldicon'); $mform->setType('oldicon', PARAM_URL); @@ -160,6 +184,11 @@ public function definition() { $mform->setAdvanced('lti_secureicon'); $mform->addHelpButton('lti_secureicon', 'secure_icon_url', 'lti'); + if (!$istool) { + // Display the lti advantage services. + $this->get_lti_advantage_services($mform); + } + if (!$istool) { // Add privacy preferences fieldset where users choose whether to send their data. $mform->addElement('header', 'privacy', get_string('privacy', 'lti')); @@ -253,4 +282,19 @@ public function get_data() { } return $data; } + + /** + * Generates the lti advantage extra configuration adding it to the mform + * + * @param MoodleQuickForm $mform + */ + public function get_lti_advantage_services(&$mform) { + // For each service add the label and get the array of configuration. + $services = lti_get_services(); + $mform->addElement('header', 'services', get_string('services', 'lti')); + foreach ($services as $service) { + /** @var \mod_lti\local\ltiservice\service_base $service */ + $service->get_configuration_options($mform); + } + } } diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 11391c1ce8855..12e68000f18d5 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -510,9 +510,10 @@ If two different tool configurations are for the same domain, the most specific match will be used. You can also insert a cartridge URL if you have one and the details for the tool will be automatically filled.'; +$string['toolurl_contentitemselectionrequest'] = 'Content Selection URL'; +$string['toolurl_contentitemselectionrequest_help'] = 'The Content Selection URL will be used to launch the content selection page from the tool provider. If it is empty, the Tool URL will be used'; $string['typename'] = 'Tool name'; -$string['typename_help'] = 'The tool name is used to identify the tool provider within Moodle. The name entered will be visible -to teachers when adding external tools within courses.'; +$string['typename_help'] = 'The tool name is used to identify the tool provider within Moodle. The name entered will be visible to teachers when adding external tools within courses.'; $string['types'] = 'Types'; $string['unabletocreatetooltype'] = 'Unable to create tool'; $string['unabletofindtooltype'] = 'Unable to find tool for {$a->id}'; diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 13ca7c5bc5f92..b501b2cf7cdff 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -53,6 +53,7 @@ // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; +global $CFG; require_once($CFG->dirroot.'/mod/lti/OAuth.php'); require_once($CFG->libdir.'/weblib.php'); require_once($CFG->dirroot . '/course/modlib.php'); @@ -96,7 +97,7 @@ * @since Moodle 3.0 */ function lti_get_launch_data($instance) { - global $PAGE, $CFG; + global $PAGE, $CFG, $USER; if (empty($instance->typeid)) { $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); @@ -230,6 +231,18 @@ function lti_get_launch_data($instance) { $requestparams['launch_presentation_return_url'] = $returnurl; + // Add the parameters configured by the LTI advantage services. + if ($typeid && !$islti2) { + $services = lti_get_services(); + foreach ($services as $service) { + $ltiadvantageparameters = $service->get_launch_parameters('basic-lti-launch-request', + $course->id, $USER->id , $typeid, $instance->id); + foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) { + $requestparams[$ltiadvantagekey] = $ltiadvantagevalue; + } + } + } + // Allow request params to be updated by sub-plugins. $plugins = core_component::get_plugin_list('ltisource'); foreach (array_keys($plugins) as $plugin) { @@ -284,7 +297,7 @@ function lti_launch_tool($instance) { /** * Prepares an LTI registration request message * - * $param object $instance Tool Proxy instance object + * @param object $toolproxy Tool Proxy instance object */ function lti_register($toolproxy) { $endpoint = $toolproxy->regurl; @@ -617,6 +630,8 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus 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) { + global $USER; + $tool = lti_get_type($id); // Validate parameters. if (!$tool) { @@ -693,6 +708,18 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu $requestparams = array_merge($requestparams, $lti2params); } + // Add the parameters configured by the LTI advantage services. + if ($id && !$islti2) { + $services = lti_get_services(); + foreach ($services as $service) { + $ltiadvantageparameters = $service->get_launch_parameters('ContentItemSelectionRequest', + $course->id, $USER->id , $id); + foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) { + $requestparams[$ltiadvantagekey] = $ltiadvantagevalue; + } + } + } + // 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'); @@ -856,9 +883,6 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver 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'); } @@ -922,7 +946,7 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver } function lti_get_tool_table($tools, $id) { - global $CFG, $OUTPUT, $USER; + global $OUTPUT; $html = ''; $typename = get_string('typename', 'lti'); @@ -1124,9 +1148,9 @@ function lti_get_tool_proxy_table($toolproxies, $id) { /** * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter * - * @param object $tool Tool instance object + * @param object $tool Tool instance object * - * @return Array of enabled capabilities + * @return array List of enabled capabilities */ function lti_get_enabled_capabilities($tool) { if (!isset($tool)) { @@ -1224,10 +1248,11 @@ function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) { * @param string $value Custom parameter value * @param boolean $islti2 True if an LTI 2 tool is being launched * - * @return Parsed value of custom parameter + * @return string Parsed value of custom parameter */ function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) { - global $USER, $COURSE; + // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER var. + global $USER; if ($value) { if (substr($value, 0, 1) == '\\') { @@ -1403,8 +1428,6 @@ function lti_get_tools_by_url($url, $state, $courseid = null) { function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { global $DB, $SITE; - $filters = array('tooldomain' => $domain); - $statefilter = ''; $coursefilter = ''; @@ -1433,6 +1456,9 @@ function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { /** * Returns all basicLTI tools configured by the administrator * + * @param int $course + * + * @return array */ function lti_filter_get_types($course) { global $DB; @@ -1698,7 +1724,7 @@ function lti_delete_type($id) { function lti_set_state_for_type($id, $state) { global $DB; - $DB->update_record('lti_types', array('id' => $id, 'state' => $state)); + $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state)); } /** @@ -1709,7 +1735,6 @@ function lti_set_state_for_type($id, $state) { * @return array Basic LTI configuration details */ function lti_get_config($ltiobject) { - $typeconfig = array(); $typeconfig = (array)$ltiobject; $additionalconfig = lti_get_type_config($ltiobject->typeid); $typeconfig = array_merge($typeconfig, $additionalconfig); @@ -1722,7 +1747,7 @@ function lti_get_config($ltiobject) { * * @param int $id * - * @return Instance configuration + * @return object configuration * */ function lti_get_type_config_from_instance($id) { @@ -1760,7 +1785,7 @@ function lti_get_type_config_from_instance($id) { * * @param int $id * - * @return Configuration details + * @return stdClass Configuration details */ function lti_get_type_type_config($id) { global $DB; @@ -1847,6 +1872,10 @@ function lti_get_type_type_config($id) { $type->lti_contentitem = $config['contentitem']; } + if (isset($config['toolurl_ContentItemSelectionRequest'])) { + $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest']; + } + if (isset($config['debuglaunch'])) { $type->lti_debuglaunch = $config['debuglaunch']; } @@ -1855,6 +1884,19 @@ function lti_get_type_type_config($id) { $type->lti_module_class_type = $config['module_class_type']; } + // Get the parameters from the LTI services. + $services = lti_get_services(); + $ltiserviceprefixlength = 11; + foreach ($services as $service) { + $configurationparameters = $service->get_configuration_parameter_names(); + foreach ($configurationparameters as $ltiserviceparameter) { + $shortltiserviceparameter = substr($ltiserviceparameter, $ltiserviceprefixlength); + if (isset($config[$shortltiserviceparameter])) { + $type->$ltiserviceparameter = $config[$shortltiserviceparameter]; + } + } + } + return $type; } @@ -1886,6 +1928,14 @@ function lti_prepare_type_for_save($type, $config) { $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0; $config->lti_contentitem = $type->contentitem; } + if (isset($config->lti_toolurl_ContentItemSelectionRequest)) { + if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) { + $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest; + } else { + $type->toolurl_ContentItemSelectionRequest = ''; + } + $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest; + } $type->timemodified = time(); @@ -1901,7 +1951,6 @@ function lti_update_type($type, $config) { lti_prepare_type_for_save($type, $config); - $clearcache = false; if (lti_request_is_using_ssl() && !empty($type->secureicon)) { $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon); } else { @@ -1918,6 +1967,13 @@ function lti_update_type($type, $config) { $record->value = $value; lti_update_config($record); } + if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) { + $record = new \StdClass(); + $record->typeid = $type->id; + $record->name = substr($key, 11); + $record->value = $value; + lti_update_config($record); + } } require_once($CFG->libdir.'/modinfolib.php'); if ($clearcache) { @@ -1964,10 +2020,17 @@ function lti_add_type($type, $config) { if ($id) { foreach ($config as $key => $value) { - if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { + if (!is_null($value)) { + $fieldparts = preg_split("/(lti|ltiservice)_/i", $key); + // If array has only one element, it did not start with the pattern. + if (count($fieldparts) < 2) { + continue; + } + $fieldname = $fieldparts[1]; + $record = new \StdClass(); $record->typeid = $id; - $record->name = substr($key, 4); + $record->name = $fieldname; $record->value = $value; lti_add_config($record); @@ -2033,7 +2096,7 @@ function lti_get_tool_proxies_from_registration_url($regurl) { * * @param int $id * - * @return Tool Proxy details + * @return mixed Tool Proxy details */ function lti_get_tool_proxy($id) { global $DB; @@ -2052,7 +2115,6 @@ function lti_get_tool_proxies($orphanedonly) { global $DB; if ($orphanedonly) { - $tools = $DB->get_records('lti_types'); $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL')); $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); foreach ($proxies as $key => $value) { @@ -2071,7 +2133,7 @@ function lti_get_tool_proxies($orphanedonly) { * * @param int $id * - * @return Tool Proxy details + * @return mixed Tool Proxy details */ function lti_get_tool_proxy_config($id) { $toolproxy = lti_get_tool_proxy($id); @@ -2190,12 +2252,11 @@ function lti_add_config($config) { * * @param object $config Tool configuration * - * @return Record id number + * @return mixed Record id number */ function lti_update_config($config) { global $DB; - $return = true; $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name)); if ($old) { @@ -2243,7 +2304,7 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); if ($record !== false) { - $DB->update_record('lti_tool_settings', array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); + $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); } else { $record = new \stdClass(); $record->toolproxyid = $toolproxyid; @@ -2259,11 +2320,12 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta /** * Signs the petition to launch the external tool using OAuth * - * @param $oldparms Parameters to be passed for signing - * @param $endpoint url of the external tool - * @param $method Method for sending the parameters (e.g. POST) - * @param $oauth_consumoer_key Key - * @param $oauth_consumoer_secret Secret + * @param array $oldparms Parameters to be passed for signing + * @param string $endpoint url of the external tool + * @param string $method Method for sending the parameters (e.g. POST) + * @param string $oauthconsumerkey + * @param string $oauthconsumersecret + * @return array|null */ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { @@ -2285,9 +2347,10 @@ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $ /** * Posts the launch petition HTML * - * @param $newparms Signed parameters - * @param $endpoint URL of the external tool - * @param $debug Debug (true/false) + * @param array $newparms Signed parameters + * @param string $endpoint URL of the external tool + * @param bool $debug Debug (true/false) + * @return string */ function lti_post_launch_html($newparms, $endpoint, $debug=false) { $r = "
course == 1) { - return; - } else { + if ($type->course != 1) { $url = new moodle_url('/course/view.php', array('id' => $type->course)); return $url->out(); } + return null; } /** @@ -2758,7 +2820,7 @@ function get_tool_type_course_url(stdClass $type) { * * @param stdClass $type The tool type * - * @return string The urls of the tool type + * @return array The urls of the tool type */ function get_tool_type_urls(stdClass $type) { $courseurl = get_tool_type_course_url($type); @@ -2780,7 +2842,7 @@ function get_tool_type_urls(stdClass $type) { * * @param stdClass $proxy The tool proxy * - * @return string The urls of the tool proxy + * @return array The urls of the tool proxy */ function get_tool_proxy_urls(stdClass $proxy) { global $OUTPUT; @@ -2802,7 +2864,6 @@ function get_tool_proxy_urls(stdClass $proxy) { * pending, configured, rejected, unknown */ function get_tool_type_state_info(stdClass $type) { - $state = ''; $isconfigured = false; $ispending = false; $isrejected = false; diff --git a/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php new file mode 100644 index 0000000000000..b53168f84e000 --- /dev/null +++ b/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php @@ -0,0 +1,137 @@ +. + +/** + * This file contains the class for restore of this gradebookservices plugin + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot.'/mod/lti/locallib.php'); + +/** + * Provides the information to backup gradebookservices lineitems + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_ltiservice_gradebookservices_subplugin extends backup_subplugin { + + /** TypeId contained in DB but is invalid */ + const NONVALIDTYPEID = 0; + + /** + * Returns the subplugin information to attach to submission element + * @return backup_subplugin_element + */ + protected function define_lti_subplugin_structure() { + global $DB; + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + // The gbs entries related with this element. + $lineitems = new backup_nested_element('lineitems'); + $lineitem = new backup_nested_element('lineitem', array('id'), array( + 'gradeitemid', + 'courseid', + 'toolproxyid', + 'typeid', + 'baseurl', + 'ltilinkid', + 'tag', + 'vendorcode', + 'guid' + ) + ); + + // Build the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($lineitems); + $lineitems->add_child($lineitem); + + // We need to know the actual activity tool or toolproxy. + // If and activity is assigned to a type that doesn't exists we don't want to backup any related lineitems.`` + // Default to invalid condition. + $typeid = 0; + $toolproxyid = '0'; + + /* cache parent property to account for missing PHPDoc type specification */ + /** @var backup_activity_task $activitytask */ + $activitytask = $this->task; + $activityid = $activitytask->get_activityid(); + $activitycourseid = $activitytask->get_courseid(); + $lti = $DB->get_record('lti', ['id' => $activityid], 'typeid, toolurl, securetoolurl'); + $ltitype = $DB->get_record('lti_types', ['id' => $lti->typeid], 'toolproxyid, baseurl'); + if ($ltitype) { + $typeid = $lti->typeid; + $toolproxyid = $ltitype->toolproxyid; + } else if ($lti->typeid == self::NONVALIDTYPEID) { // This activity comes from an old backup. + // 1. Let's check if the activity is coupled. If so, find the values in the GBS element. + $gbsrecord = $DB->get_record('ltiservice_gradebookservices', + ['ltilinkid' => $activityid], 'typeid,toolproxyid,baseurl'); + if ($gbsrecord) { + $typeid = $gbsrecord->typeid; + $toolproxyid = $gbsrecord->toolproxyid; + } else { // 2. If it is uncoupled... we will need to guess the right activity typeid + // Guess the typeid for the activity. + $tool = lti_get_tool_by_url_match($lti->toolurl, $activitycourseid); + if (!$tool) { + $tool = lti_get_tool_by_url_match($lti->securetoolurl, $activitycourseid); + } + if ($tool) { + $alttypeid = $tool->id; + // If we have a valid typeid then get types again. + if ($alttypeid != self::NONVALIDTYPEID) { + $ltitype = $DB->get_record('lti_types', ['id' => $alttypeid], 'toolproxyid, baseurl'); + $toolproxyid = $ltitype->toolproxyid; + } + } + } + } + + // Define sources. + if ($toolproxyid != null) { + $lineitemssql = "SELECT l.*, t.vendorcode as vendorcode, t.guid as guid + FROM {ltiservice_gradebookservices} l + INNER JOIN {lti_tool_proxies} t ON (t.id = l.toolproxyid) + WHERE l.courseid = ? + AND l.toolproxyid = ? + AND l.typeid is null"; + $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($toolproxyid)]; + } else { + $lineitemssql = "SELECT l.*, null as vendorcode, null as guid + FROM {ltiservice_gradebookservices} l + WHERE l.courseid = ? + AND l.typeid = ? + AND l.toolproxyid is null"; + $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($typeid)]; + } + + $lineitem->set_source_sql($lineitemssql, $lineitemsparams); + + return $subplugin; + } +} diff --git a/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php new file mode 100644 index 0000000000000..35a16f6041598 --- /dev/null +++ b/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php @@ -0,0 +1,209 @@ +. + +/** + * This file contains the class for restore of this gradebookservices plugin + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/mod/lti/locallib.php'); + +/** + * Restore subplugin class. + * + * Provides the necessary information + * needed to restore the lineitems related with the lti activity (coupled), + * and all the uncoupled ones from the course. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_ltiservice_gradebookservices_subplugin extends restore_subplugin { + + /** + * Returns the subplugin structure to attach to the XML element. + * + * @return restore_path_element[] array of elements to be processed on restore. + */ + protected function define_lti_subplugin_structure() { + + $paths = array(); + $elename = $this->get_namefor('lineitem'); + $elepath = $this->get_pathfor('/lineitems/lineitem'); + $paths[] = new restore_path_element($elename, $elepath); + return $paths; + } + + /** + * Processes one lineitem + * + * @param mixed $data + * @return void + */ + public function process_ltiservice_gradebookservices_lineitem($data) { + global $DB; + $data = (object)$data; + // The coupled lineitems are restored as any other grade item + // so we will only create the entry in the ltiservice_gradebookservices table. + // We will try to find a valid toolproxy in the system. + // If it has been found before... we use it. + /* cache parent property to account for missing PHPDoc type specification */ + /** @var backup_activity_task $activitytask */ + $activitytask = $this->task; + $courseid = $activitytask->get_courseid(); + if ($data->typeid != null) { + if ($ltitypeid = $this->get_mappingid('ltitype', $data->typeid)) { + $newtypeid = $ltitypeid; + } else { // If not, then we will call our own function to find it. + $newtypeid = $this->find_typeid($data, $courseid); + } + } else { + $newtypeid = null; + } + if ($data->toolproxyid != null) { + $ltitoolproxy = $this->get_mappingid('ltitoolproxy', $data->toolproxyid); + if ($ltitoolproxy && $ltitoolproxy != 0) { + $newtoolproxyid = $ltitoolproxy; + } else { // If not, then we will call our own function to find it. + $newtoolproxyid = $this->find_proxy_id($data); + } + } else { + $newtoolproxyid = null; + } + if ($data->ltilinkid != null) { + $ltilinkid = $this->get_new_parentid('lti'); + } else { + $ltilinkid = null; + } + // If this has not been restored before. + if ($this->get_mappingid('gbsgradeitemrestored', $data->id, 0) == 0) { + $newgbsid = $DB->insert_record('ltiservice_gradebookservices', (object) array( + 'gradeitemid' => 0, + 'courseid' => $courseid, + 'toolproxyid' => $newtoolproxyid, + 'ltilinkid' => $ltilinkid, + 'typeid' => $newtypeid, + 'baseurl' => $data->baseurl, + 'tag' => $data->tag + )); + $this->set_mapping('gbsgradeitemoldid', $newgbsid, $data->gradeitemid); + $this->set_mapping('gbsgradeitemrestored', $data->id, $data->id); + } + } + + /** + * If the toolproxy is not in the mapping (or it is 0) + * we try to find the toolproxyid. + * If none is found, then we set it to 0. + * + * @param mixed $data + * @return integer $newtoolproxyid + */ + private function find_proxy_id($data) { + global $DB; + $newtoolproxyid = 0; + $oldtoolproxyguid = $data->guid; + $oldtoolproxyvendor = $data->vendorcode; + + $dbtoolproxyjsonparams = array('guid' => $oldtoolproxyguid, 'vendorcode' => $oldtoolproxyvendor); + $dbtoolproxy = $DB->get_field('lti_tool_proxies', 'id', $dbtoolproxyjsonparams, IGNORE_MISSING); + if ($dbtoolproxy) { + $newtoolproxyid = $dbtoolproxy; + } + return $newtoolproxyid; + } + + /** + * If the typeid is not in the mapping or it is 0, (it should be most of the times) + * we will try to find the better typeid that matches with the lineitem. + * If none is found, then we set it to 0. + * + * @param stdClass $data + * @param int $courseid + * @return int The item type id + */ + private function find_typeid($data, $courseid) { + global $DB; + $newtypeid = 0; + $oldtypeid = $data->typeid; + + // 1. Find a type with the same id in the same course. + $dbtypeidparameter = array('id' => $oldtypeid, 'course' => $courseid, 'baseurl' => $data->baseurl); + $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id + AND course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl", + $dbtypeidparameter); + if ($dbtype) { + $newtypeid = $dbtype; + } else { + // 2. Find a site type for all the courses (course == 1), but with the same id. + $dbtypeidparameter = array('id' => $oldtypeid, 'baseurl' => $data->baseurl); + $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id + AND course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl", + $dbtypeidparameter); + if ($dbtype) { + $newtypeid = $dbtype; + } else { + // 3. Find a type with the same baseurl in the actual site. + $dbtypeidparameter = array('course' => $courseid, 'baseurl' => $data->baseurl); + $dbtype = $DB->get_field_select('lti_types', 'id', "course=:course + AND ".$DB->sql_compare_text('baseurl')."=:baseurl", + $dbtypeidparameter); + if ($dbtype) { + $newtypeid = $dbtype; + } else { + // 4. Find a site type for all the courses (course == 1) with the same baseurl. + $dbtypeidparameter = array('course' => 1, 'baseurl' => $data->baseurl); + $dbtype = $DB->get_field_select('lti_types', 'id', "course=1 + AND ".$DB->sql_compare_text('baseurl')."=:baseurl", + $dbtypeidparameter); + if ($dbtype) { + $newtypeid = $dbtype; + } + } + } + } + return $newtypeid; + } + + /** + * We call the after_restore_lti to update the grade_items id's that we didn't know in the moment of creating + * the gradebookservices rows. + */ + protected function after_restore_lti() { + global $DB; + $activitytask = $this->task; + $courseid = $activitytask->get_courseid(); + $gbstoupdate = $DB->get_records('ltiservice_gradebookservices', array('gradeitemid' => 0, 'courseid' => $courseid)); + foreach ($gbstoupdate as $gbs) { + $oldgradeitemid = $this->get_mappingid('gbsgradeitemoldid', $gbs->id, 0); + $newgradeitemid = $this->get_mappingid('grade_item', $oldgradeitemid, 0); + if ($newgradeitemid > 0) { + $gbs->gradeitemid = $newgradeitemid; + $DB->update_record('ltiservice_gradebookservices', $gbs); + } + } + } + +} diff --git a/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php b/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php new file mode 100644 index 0000000000000..16a3620e74e18 --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php @@ -0,0 +1,365 @@ +. + +/** + * This file contains a class definition for the LineItem resource + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\local\resources; + +use ltiservice_gradebookservices\local\service\gradebookservices; +use mod_lti\local\ltiservice\resource_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A resource implementing LineItem. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lineitem extends resource_base { + + /** + * Class constructor. + * + * @param gradebookservices $service Service instance + */ + public function __construct($service) { + + parent::__construct($service); + $this->id = 'LineItem.item'; + $this->template = '/{context_id}/lineitems/{item_id}/lineitem'; + $this->variables[] = 'LineItem.url'; + $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; + $this->methods[] = self::HTTP_GET; + $this->methods[] = self::HTTP_PUT; + $this->methods[] = self::HTTP_DELETE; + + } + + /** + * Execute the request for this resource. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + */ + public function execute($response) { + global $CFG, $DB; + + $params = $this->parse_template(); + $contextid = $params['context_id']; + $itemid = $params['item_id']; + if ($response->get_request_method() === 'GET') { + $contenttype = $response->get_accept(); + } else { + $contenttype = $response->get_content_type(); + } + // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2. + $typeid = optional_param('type_id', null, PARAM_ALPHANUM); + if (is_null($typeid)) { + if (!$this->check_tool_proxy(null, $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("Invalid tool proxy specified."); + return; + } + } else { + switch ($response->get_request_method()) { + case self::HTTP_GET: + if (!$this->check_type($typeid, $contextid, 'LineItem.item:get', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support GET requests."); + return; + } + break; + case self::HTTP_PUT: + if (!$this->check_type($typeid, $contextid, 'LineItem.item:put', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support PUT requests."); + return; + } + break; + case self::HTTP_DELETE: + if (!$this->check_type($typeid, $contextid, 'LineItem.item:delete', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support DELETE requests."); + return; + } + break; + default: // Should not be possible. + $response->set_code(405); + return; + } + } + if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { + $response->set_code(400); + $response->set_reason("Invalid request made."); + return; + } + if (!$DB->record_exists('course', array('id' => $contextid))) { + $response->set_code(404); + $response->set_reason("Not Found: Course $contextid doesn't exist."); + return; + } + if (!$DB->record_exists('grade_items', array('id' => $itemid))) { + $response->set_code(404); + $response->set_reason("Not Found: Grade item $itemid doesn't exist."); + return; + } + $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); + if ($item === false) { + $response->set_code(403); + $response->set_reason("Line item does not exist."); + return; + } + require_once($CFG->libdir.'/gradelib.php'); + switch ($response->get_request_method()) { + case 'GET': + $this->get_request($response, $item, $typeid); + break; + case 'PUT': + $json = $this->process_put_request($response->get_request_data(), $item, $typeid); + $response->set_body($json); + $response->set_code(200); + break; + case 'DELETE': + $this->process_delete_request($item); + $response->set_code(204); + break; + default: // Should not be possible. + $response->set_code(405); + $response->set_reason("Invalid request method specified."); + return; + } + } + + /** + * Process a GET request. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + * @param object $item Grade item instance. + * @param string $typeid Tool Type Id + */ + private function get_request($response, $item, $typeid) { + + $response->set_content_type($this->formats[0]); + $lineitem = gradebookservices::item_for_json($item, substr(parent::get_endpoint(), + 0, strrpos(parent::get_endpoint(), "/", -10)), $typeid); + $response->set_body(json_encode($lineitem)); + + } + + /** + * Process a PUT request. + * + * @param string $body PUT body + * @param \ltiservice_gradebookservices\local\resources\lineitem $olditem Grade item instance + * @param string $typeid Tool Type Id + * + * @return string + * @throws \Exception + */ + private function process_put_request($body, $olditem, $typeid) { + global $DB; + $json = json_decode($body); + if (empty($json) || + !isset($json->scoreMaximum) || + !isset($json->label)) { + throw new \Exception(null, 400); + } + $item = \grade_item::fetch(array('id' => $olditem->id, 'courseid' => $olditem->courseid)); + $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($olditem->id); + $updategradeitem = false; + $rescalegrades = false; + $oldgrademax = grade_floatval($item->grademax); + $upgradegradebookservices = false; + if ($item->itemname !== $json->label) { + $updategradeitem = true; + } + $item->itemname = $json->label; + if (!is_numeric($json->scoreMaximum)) { + throw new \Exception(null, 400); + } else { + if (grade_floats_different($oldgrademax, + grade_floatval($json->scoreMaximum))) { + $updategradeitem = true; + $rescalegrades = true; + } + $item->grademax = grade_floatval($json->scoreMaximum); + } + $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; + if ($item->idnumber !== $resourceid) { + $updategradeitem = true; + } + $item->idnumber = $resourceid; + if ($gbs) { + $tag = (isset($json->tag)) ? $json->tag : null; + if ($gbs->tag !== $tag) { + $upgradegradebookservices = true; + } + $gbs->tag = $tag; + } + $ltilinkid = null; + if (isset($json->ltiLinkId)) { + if (is_numeric($json->ltiLinkId)) { + $ltilinkid = $json->ltiLinkId; + if ($gbs) { + if (intval($gbs->ltilinkid) !== intval($json->ltiLinkId)) { + $gbs->ltilinkid = $json->ltiLinkId; + $upgradegradebookservices = true; + } + } else { + if (intval($item->iteminstance) !== intval($json->ltiLinkId)) { + $item->iteminstance = intval($json->ltiLinkId); + $updategradeitem = true; + } + } + } else { + throw new \Exception(null, 400); + } + } + if ($ltilinkid != null) { + if (is_null($typeid)) { + if (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, + $this->get_service()->get_tool_proxy()->id)) { + throw new \Exception(null, 403); + } + } else { + if (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, + $typeid)) { + throw new \Exception(null, 403); + } + } + } + if ($updategradeitem) { + if (!$item->update('mod/ltiservice_gradebookservices')) { + throw new \Exception(null, 500); + } + if ($rescalegrades) { + $item->rescale_grades_keep_percentage(0, $oldgrademax, 0, $item->grademax); + } + } + + $lineitem = new lineitem($this->get_service()); + $endpoint = $lineitem->get_endpoint(); + + if ($upgradegradebookservices) { + if (is_null($typeid)) { + $toolproxyid = $this->get_service()->get_tool_proxy()->id; + $baseurl = null; + } else { + $toolproxyid = null; + $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; + } + $DB->update_record('ltiservice_gradebookservices', (object)array( + 'id' => $gbs->id, + 'gradeitemid' => $gbs->gradeitemid, + 'courseid' => $gbs->courseid, + 'toolproxyid' => $toolproxyid, + 'typeid' => $typeid, + 'baseurl' => $baseurl, + 'ltilinkid' => $ltilinkid, + 'tag' => $gbs->tag + )); + } + + if (is_null($typeid)) { + $id = "{$endpoint}"; + $json->id = $id; + } else { + $id = "{$endpoint}?type_id={$typeid}"; + $json->id = $id; + } + return json_encode($json, JSON_UNESCAPED_SLASHES); + + } + + /** + * Process a DELETE request. + * + * @param \ltiservice_gradebookservices\local\resources\lineitem $item Grade item instance + * @throws \Exception + */ + private function process_delete_request($item) { + global $DB; + + $gradeitem = \grade_item::fetch(array('id' => $item->id)); + if (($gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($item->id)) == false) { + throw new \Exception(null, 403); + } + if (!$gradeitem->delete('mod/ltiservice_gradebookservices')) { + throw new \Exception(null, 500); + } else { + $sqlparams = array(); + $sqlparams['id'] = $gbs->id; + if (!$DB->delete_records('ltiservice_gradebookservices', $sqlparams)) { + throw new \Exception(null, 500); + } + } + } + + /** + * Get permissions from the config of the tool for that resource + * + * @param int $typeid + * + * @return array with the permissions related to this resource by the $lti_type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->ltiservice_gradesynchronization == '1') { + return array('LineItem.item:get'); + } else if ($tool->ltiservice_gradesynchronization == '2') { + return array('LineItem.item:get', 'LineItem.item:put', 'LineItem.item:delete'); + } else { + return array(); + } + } + + /** + * Parse a value for custom parameter substitution variables. + * + * @param string $value String to be parsed + * + * @return string + */ + public function parse_value($value) { + global $COURSE, $CFG; + if (strpos($value, '$LineItem.url') !== false) { + $resolved = ''; + require_once($CFG->libdir . '/gradelib.php'); + + $this->params['context_id'] = $COURSE->id; + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); + $id = $cm->instance; + $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); + if ($item && $item->items) { + $this->params['item_id'] = $item->items[0]->id; + $resolved = parent::get_endpoint(); + } + } + $value = str_replace('$LineItem.url', $resolved, $value); + } + return $value; + } +} diff --git a/mod/lti/service/gradebookservices/classes/local/resources/lineitems.php b/mod/lti/service/gradebookservices/classes/local/resources/lineitems.php new file mode 100644 index 0000000000000..cd70180adefdc --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/local/resources/lineitems.php @@ -0,0 +1,352 @@ +. + +/** + * This file contains a class definition for the LineItem container resource + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\local\resources; + +use ltiservice_gradebookservices\local\service\gradebookservices; +use mod_lti\local\ltiservice\resource_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A resource implementing LineItem container. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lineitems extends resource_base { + + /** + * Class constructor. + * + * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance + */ + public function __construct($service) { + + parent::__construct($service); + $this->id = 'LineItem.collection'; + $this->template = '/{context_id}/lineitems'; + $this->variables[] = 'LineItems.url'; + $this->formats[] = 'application/vnd.ims.lis.v2.lineitemcontainer+json'; + $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json'; + $this->methods[] = self::HTTP_GET; + $this->methods[] = self::HTTP_POST; + + } + + /** + * Execute the request for this resource. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + */ + public function execute($response) { + global $DB; + + $params = $this->parse_template(); + $contextid = $params['context_id']; + $isget = $response->get_request_method() === self::HTTP_GET; + if ($isget) { + $contenttype = $response->get_accept(); + } else { + $contenttype = $response->get_content_type(); + } + $container = empty($contenttype) || ($contenttype === $this->formats[0]); + // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. + $typeid = optional_param('type_id', null, PARAM_ALPHANUM); + if (is_null($typeid)) { + if (!$this->check_tool_proxy(null, $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("Invalid tool proxy specified."); + return; + } + } else { + switch ($response->get_request_method()) { + case self::HTTP_GET: + if (!$this->check_type($typeid, $contextid, 'LineItem.collection:get', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support GET requests."); + return; + } + break; + case self::HTTP_POST: + if (!$this->check_type($typeid, $contextid, 'LineItem.collection:post', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support POST requests."); + return; + } + break; + default: // Should not be possible. + $response->set_code(405); + $response->set_reason("Invalid request method specified."); + return; + } + } + if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) || + (!empty($contenttype) && !in_array($contenttype, $this->formats))) { + $response->set_code(400); + $response->set_reason("Invalid request made."); + return; + } + if (!$DB->record_exists('course', array('id' => $contextid))) { + $response->set_code(404); + $response->set_reason("Not Found: Course $contextid doesn't exist."); + return; + } + switch ($response->get_request_method()) { + case self::HTTP_GET: + $resourceid = optional_param('resource_id', null, PARAM_TEXT); + $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT); + $tag = optional_param('tag', null, PARAM_TEXT); + $limitnum = optional_param('limit', 0, PARAM_INT); + $limitfrom = optional_param('from', 0, PARAM_INT); + $itemsandcount = $this->get_service()->get_lineitems($contextid, $resourceid, $ltilinkid, $tag, $limitfrom, + $limitnum, $typeid); + $items = $itemsandcount[1]; + $totalcount = $itemsandcount[0]; + $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom, + $limitnum, $totalcount, $typeid, $response); + $response->set_content_type($this->formats[0]); + break; + case self::HTTP_POST: + try { + $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid); + $response->set_code(201); + $response->set_content_type($this->formats[1]); + } catch (\Exception $e) { + $response->set_code($e->getCode()); + $response->set_reason($e->getMessage()); + } + break; + default: // Should not be possible. + $response->set_code(405); + $response->set_reason("Invalid request method specified."); + return; + } + $response->set_body($json); + } + + /** + * Generate the JSON for a GET request. + * + * @param array $items Array of lineitems + * @param string $resourceid Resource identifier used for filtering, may be null + * @param string $ltilinkid Resource Link identifier used for filtering, may be null + * @param string $tag Tag identifier used for filtering, may be null + * @param int $limitfrom Offset of the first line item to return + * @param int $limitnum Maximum number of line items to return, ignored if zero or less + * @param int $totalcount Number of total lineitems before filtering for paging + * @param int $typeid Maximum number of line items to return, ignored if zero or less + * @param \mod_lti\local\ltiservice\response $response + + * @return string + */ + private function get_json_for_get_request($items, $resourceid, $ltilinkid, + $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) { + + $firstpage = null; + $nextpage = null; + $prevpage = null; + $lastpage = null; + if (isset($limitnum) && $limitnum > 0) { + if ($limitfrom >= $totalcount || $limitfrom < 0) { + $outofrange = true; + } else { + $outofrange = false; + } + $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; + $limitcurrent = $limitfrom; + $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; + $limitfrom += $limitnum; + + $baseurl = new \moodle_url($this->get_endpoint()); + if (isset($resourceid)) { + $baseurl->param('resource_id', $resourceid); + } + if (isset($ltilinkid)) { + $baseurl->param('lti_link_id', $ltilinkid); + } + if (isset($tag)) { + $baseurl->param('tag', $tag); + } + + if (is_null($typeid)) { + $baseurl->param('limit', $limitnum); + if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { + $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); + } + $firstpage = new \moodle_url($baseurl, ['from' => 0]); + $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); + $lastpage = new \moodle_url($baseurl, ['from' > $limitlast]); + if (($limitcurrent > 0) && (!$outofrange)) { + $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); + } + } else { + $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); + if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { + $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); + } + $firstpage = new \moodle_url($baseurl, ['from' => 0]); + $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); + $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); + if (($limitcurrent > 0) && (!$outofrange)) { + $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); + } + } + } + + $jsonitems=[]; + $endpoint = parent::get_endpoint(); + foreach ($items as $item) { + array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid)); + } + + if (isset($canonicalpage) && ($canonicalpage)) { + $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; + if (!is_null($prevpage)) { + $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; + } + $links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”'; + if (!is_null($nextpage)) { + $links .= ', <' . $nextpage->out() . '>; rel=“next”'; + } + $links .= ', <' . $lastpage->out() . '>; rel=“last”'; + $response->add_additional_header($links); + } + return json_encode($jsonitems); + } + + /** + * Generate the JSON for a POST request. + * + * @param string $body POST body + * @param string $contextid Course ID + * @param string $typeid + * + * @return string + * @throws \Exception + */ + private function get_json_for_post_request($body, $contextid, $typeid) { + global $CFG, $DB; + + $json = json_decode($body); + if (empty($json) || + !isset($json->scoreMaximum) || + !isset($json->label)) { + throw new \Exception(null, 400); + } + if (is_numeric($json->scoreMaximum)) { + $max = $json->scoreMaximum; + } else { + throw new \Exception(null, 400); + } + require_once($CFG->libdir.'/gradelib.php'); + $resourceid = (isset($json->resourceId)) ? $json->resourceId : ''; + $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null; + if ($ltilinkid != null) { + if (is_null($typeid)) { + if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) { + throw new \Exception(null, 403); + } + } else { + if (!gradebookservices::check_lti_1x_id($ltilinkid, $contextid, $typeid)) { + throw new \Exception(null, 403); + } + } + } + $tag = (isset($json->tag)) ? $json->tag : ''; + if (is_null($typeid)) { + $toolproxyid = $this->get_service()->get_tool_proxy()->id; + $baseurl = null; + } else { + $toolproxyid = null; + $baseurl = lti_get_type_type_config($typeid)->lti_toolurl; + } + $params = array(); + $params['itemname'] = $json->label; + $params['gradetype'] = GRADE_TYPE_VALUE; + $params['grademax'] = $max; + $params['grademin'] = 0; + $item = new \grade_item(array('id' => 0, 'courseid' => $contextid)); + \grade_item::set_properties($item, $params); + $item->itemtype = 'manual'; + $item->idnumber = $resourceid; + $item->grademax = $max; + $id = $item->insert('mod/ltiservice_gradebookservices'); + $DB->insert_record('ltiservice_gradebookservices', (object)array( + 'gradeitemid' => $id, + 'courseid' => $contextid, + 'toolproxyid' => $toolproxyid, + 'typeid' => $typeid, + 'baseurl' => $baseurl, + 'ltilinkid' => $ltilinkid, + 'tag' => $tag + )); + if (is_null($typeid)) { + $json->id = parent::get_endpoint() . "/{$id}/lineitem"; + } else { + $json->id = parent::get_endpoint() . "/{$id}/lineitem?type_id={$typeid}"; + } + return json_encode($json, JSON_UNESCAPED_SLASHES); + + } + + /** + * get permissions from the config of the tool for that resource + * + * @param string $typeid + * + * @return array with the permissions related to this resource by the lti type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->ltiservice_gradesynchronization == '1') { + return array('LineItem.collection:get'); + } else if ($tool->ltiservice_gradesynchronization == '2') { + return array('LineItem.collection:get', 'LineItem.collection:post'); + } else { + return array(); + } + } + + /** + * Parse a value for custom parameter substitution variables. + * + * @param string $value String to be parsed + * + * @return string + */ + public function parse_value($value) { + global $COURSE; + + if (strpos($value, '$LineItems.url') !== false) { + $this->params['context_id'] = $COURSE->id; + $value = str_replace('$LineItems.url', parent::get_endpoint(), $value); + } + + return $value; + + } +} diff --git a/mod/lti/service/gradebookservices/classes/local/resources/results.php b/mod/lti/service/gradebookservices/classes/local/resources/results.php new file mode 100644 index 0000000000000..445c1dc91f49a --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/local/resources/results.php @@ -0,0 +1,304 @@ +. + +/** + * This file contains a class definition for the LISResults container resource + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\local\resources; + +use ltiservice_gradebookservices\local\service\gradebookservices; +use mod_lti\local\ltiservice\resource_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A resource implementing LISResults container. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class results extends resource_base { + + /** + * Class constructor. + * + * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance + */ + public function __construct($service) { + + parent::__construct($service); + $this->id = 'Result.collection'; + $this->template = '/{context_id}/lineitems/{item_id}/lineitem/results'; + $this->variables[] = 'Results.url'; + $this->formats[] = 'application/vnd.ims.lis.v2.resultcontainer+json'; + $this->methods[] = 'GET'; + } + + /** + * Execute the request for this resource. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + */ + public function execute($response) { + global $CFG, $DB; + + $params = $this->parse_template(); + $contextid = $params['context_id']; + $itemid = $params['item_id']; + + $isget = $response->get_request_method() === 'GET'; + if ($isget) { + $contenttype = $response->get_accept(); + } else { + $contenttype = $response->get_content_type(); + } + // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. + $typeid = optional_param('type_id', null, PARAM_ALPHANUM); + if (is_null($typeid)) { + if (!$this->check_tool_proxy(null, $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("Invalid tool proxy specified."); + return; + } + } else { + if (!$this->check_type($typeid, $contextid, 'Result.collection:get', $response->get_request_data())) { + $response->set_code(403); + $response->set_reason("This resource does not support GET requests."); + return; + } + } + if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) { + $response->set_code(400); + $response->set_reason("Invalid request made."); + return; + } + if (!$DB->record_exists('course', array('id' => $contextid))) { + $response->set_code(404); + $response->set_reason("Not Found: Course $contextid doesn't exist."); + return; + } + if (!$DB->record_exists('grade_items', array('id' => $itemid))) { + $response->set_code(404); + $response->set_reason("Not Found: Grade item $itemid doesn't exist."); + return; + } + $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); + if ($item === false) { + $response->set_code(403); + $response->set_reason("Line item does not exist."); + return; + } + $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); + $ltilinkid = null; + if (isset($item->iteminstance)) { + $ltilinkid = $item->iteminstance; + } else if ($gbs && isset($gbs->ltilinkid)) { + $ltilinkid = $gbs->ltilinkid; + } + if ($ltilinkid != null) { + if (is_null($typeid)) { + if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, + $this->get_service()->get_tool_proxy()->id))) { + $response->set_code(403); + $response->set_reason("Invalid LTI id supplied."); + return; + } + } else { + if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, + $typeid))) { + $response->set_code(403); + $response->set_reason("Invalid LTI id supplied."); + return; + } + } + } + require_once($CFG->libdir.'/gradelib.php'); + switch ($response->get_request_method()) { + case 'GET': + $useridfilter = optional_param('user_id', 0, PARAM_INT); + $limitnum = optional_param('limit', 0, PARAM_INT); + $limitfrom = optional_param('from', 0, PARAM_INT); + $typeid = optional_param('type_id', null, PARAM_TEXT); + $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum, + $useridfilter, $typeid, $response); + $response->set_content_type($this->formats[0]); + $response->set_body($json); + break; + default: // Should not be possible. + $response->set_code(405); + $response->set_reason("Invalid request method specified."); + return; + } + $response->set_body($json); + } + + /** + * Generate the JSON for a GET request. + * + * @param int $itemid Grade item instance ID + * @param int $limitfrom Offset for the first result to include in this paged set + * @param int $limitnum Maximum number of results to include in the response, ignored if zero + * @param int $useridfilter The user id to filter the results. + * @param int $typeid Lti tool typeid (or null) + * @param \mod_lti\local\ltiservice\response $response The response element needed to add a header. + * + * @return string + */ + private function get_json_for_get_request($itemid, $limitfrom, $limitnum, $useridfilter, $typeid, $response) { + + if ($useridfilter > 0) { + $grades = \grade_grade::fetch_all(array('itemid' => $itemid, 'userid' => $useridfilter)); + } else { + $grades = \grade_grade::fetch_all(array('itemid' => $itemid)); + } + + $firstpage = null; + $nextpage = null; + $prevpage = null; + $lastpage = null; + if ($grades && isset($limitnum) && $limitnum > 0) { + // Since we only display grades that have been modified, we need to filter first in order to support + // paging. + $resultgrades = array_filter($grades, function ($grade) { + return !empty($grade->timemodified); + }); + // We save the total count to calculate the last page. + $totalcount = count($resultgrades); + // We slice to the requested item offset to insure proper item is always first, and we always return + // first pageset of any remaining items. + $grades = array_slice($resultgrades, $limitfrom); + if (count($grades) > 0) { + $pagedgrades = array_chunk($grades, $limitnum); + $pageset = 0; + $grades = $pagedgrades[$pageset]; + } + if ($limitfrom >= $totalcount || $limitfrom < 0) { + $outofrange = true; + } else { + $outofrange = false; + } + $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0; + $limitcurrent = $limitfrom; + $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0; + $limitfrom += $limitnum; + + $baseurl = new \moodle_url($this->get_endpoint()); + if (is_null($typeid)) { + $baseurl->param('limit', $limitnum); + + if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { + $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); + } + $firstpage = new \moodle_url($baseurl, ['from' => 0]); + $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); + $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]); + if (($limitcurrent > 0) && (!$outofrange)) { + $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); + } + } else { + $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]); + + if (($limitfrom <= $totalcount - 1) && (!$outofrange)) { + $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]); + } + $firstpage = new \moodle_url($baseurl, ['from' => 0]); + $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]); + if (($limitcurrent > 0) && (!$outofrange)) { + $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]); + } + } + } + + $jsonresults = []; + $lineitem = new lineitem($this->get_service()); + $endpoint = $lineitem->get_endpoint(); + if ($grades) { + foreach ($grades as $grade) { + if (!empty($grade->timemodified)) { + array_push($jsonresults, gradebookservices::result_for_json($grade, $endpoint, $typeid)); + } + } + } + + if (isset($canonicalpage) && ($canonicalpage)) { + $links = 'Link: <' . $firstpage->out() . '>; rel=“first”'; + if (!is_null($prevpage)) { + $links .= ', <' . $prevpage->out() . '>; rel=“prev”'; + } + $links .= ', <' . $canonicalpage->out() . '>; rel=“canonical”'; + if (!is_null($nextpage)) { + $links .= ', <' . $nextpage->out() . '>; rel=“next”'; + } + $links .= ', <' . $lastpage->out() . '>; rel=“last”'; + $response->add_additional_header($links); + } + return json_encode($jsonresults); + } + + /** + * get permissions from the config of the tool for that resource + * + * @param int $typeid + * + * @return array with the permissions related to this resource by the $lti_type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->ltiservice_gradesynchronization == '1') { + return array('Result.collection:get'); + } else if ($tool->ltiservice_gradesynchronization == '2') { + return array('Result.collection:get'); + } else { + return array(); + } + } + + /** + * Parse a value for custom parameter substitution variables. + * + * @param string $value String to be parsed + * + * @return string + */ + public function parse_value($value) { + global $COURSE, $CFG; + if (strpos($value, '$Results.url') !== false) { + require_once($CFG->libdir . '/gradelib.php'); + + $resolved = ''; + $this->params['context_id'] = $COURSE->id; + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); + $id = $cm->instance; + $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); + if ($item && $item->items) { + $this->params['item_id'] = $item->items[0]->id; + $resolved = parent::get_endpoint(); + } + } + $value = str_replace('$Results.url', $resolved, $value); + } + return $value; + } +} diff --git a/mod/lti/service/gradebookservices/classes/local/resources/scores.php b/mod/lti/service/gradebookservices/classes/local/resources/scores.php new file mode 100644 index 0000000000000..afe63eae98940 --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/local/resources/scores.php @@ -0,0 +1,270 @@ +. + +/** + * This file contains a class definition for the LISResult container resource + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\local\resources; + +use ltiservice_gradebookservices\local\service\gradebookservices; +use mod_lti\local\ltiservice\resource_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A resource implementing LISResult container. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scores extends resource_base { + + /** + * Class constructor. + * + * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance + */ + public function __construct($service) { + + parent::__construct($service); + $this->id = 'Score.collection'; + $this->template = '/{context_id}/lineitems/{item_id}/lineitem/scores'; + $this->variables[] = 'Scores.url'; + $this->formats[] = 'application/vnd.ims.lis.v1.scorecontainer+json'; + $this->formats[] = 'application/vnd.ims.lis.v1.score+json'; + $this->methods[] = 'POST'; + + } + + /** + * Execute the request for this resource. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + */ + public function execute($response) { + global $CFG, $DB; + + $params = $this->parse_template(); + $contextid = $params['context_id']; + $itemid = $params['item_id']; + + // GET is disabled by the moment, but we have the code ready + // for a future implementation. + + $isget = $response->get_request_method() === 'GET'; + if ($isget) { + $contenttype = $response->get_accept(); + } else { + $contenttype = $response->get_content_type(); + } + $container = empty($contenttype) || ($contenttype === $this->formats[0]); + // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2. + $typeid = optional_param('type_id', null, PARAM_ALPHANUM); + if (is_null($typeid)) { + if (!$this->check_tool_proxy(null, $response->get_request_data())) { + $response->set_code(403); + return; + } + } else { + switch ($response->get_request_method()) { + case 'GET': + $response->set_code(405); + $response->set_reason("GET requests are not allowed."); + return; + case 'POST': + if (!$this->check_type($typeid, $contextid, 'Score.collection:post', $response->get_request_data())) { + $response->set_code(401); + $response->set_reason("This resource does not support POST requests."); + return; + } + break; + default: // Should not be possible. + $response->set_code(405); + return; + } + } + if (empty($contextid) || !($container ^ ($response->get_request_method() === 'POST')) || + (!empty($contenttype) && !in_array($contenttype, $this->formats))) { + $response->set_code(400); + return; + } + if (!$DB->record_exists('course', array('id' => $contextid))) { + $response->set_code(404); + $response->set_reason("Not Found: Course $contextid doesn't exist."); + return; + } + if (!$DB->record_exists('grade_items', array('id' => $itemid))) { + $response->set_code(404); + $response->set_reason("Not Found: Grade item $itemid doesn't exist."); + return; + } + $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid); + if ($item === false) { + $response->set_code(403); + $response->set_reason("Line item does not exist."); + return; + } + $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid); + $ltilinkid = null; + if (isset($item->iteminstance)) { + $ltilinkid = $item->iteminstance; + } else if ($gbs && isset($gbs->ltilinkid)) { + $ltilinkid = $gbs->ltilinkid; + } + if ($ltilinkid != null) { + if (is_null($typeid)) { + if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid, + $this->get_service()->get_tool_proxy()->id))) { + $response->set_code(403); + $response->set_reason("Invalid LTI id supplied."); + return; + } + } else { + if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid, + $typeid))) { + $response->set_code(403); + $response->set_reason("Invalid LTI id supplied."); + return; + } + } + } + $json = '[]'; + require_once($CFG->libdir.'/gradelib.php'); + switch ($response->get_request_method()) { + case 'GET': + $response->set_code(405); + $response->set_reason("GET requests are not allowed."); + break; + case 'POST': + try { + $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid, $typeid); + $response->set_content_type($this->formats[1]); + } catch (\Exception $e) { + $response->set_code($e->getCode()); + $response->set_reason($e->getMessage()); + } + break; + default: // Should not be possible. + $response->set_code(405); + $response->set_reason("Invalid request method specified."); + return; + } + $response->set_body($json); + } + + /** + * Generate the JSON for a POST request. + * + * @param \mod_lti\local\ltiservice\response $response Response object for this request. + * @param string $body POST body + * @param object $item Grade item instance + * @param string $contextid + * @param string $typeid + * + * @throws \Exception + */ + private function get_json_for_post_request($response, $body, $item, $contextid, $typeid) { + $score = json_decode($body); + if (empty($score) || + !isset($score->userId) || + !isset($score->timestamp) || + !isset($score->gradingProgress) || + !isset($score->activityProgress) || + !isset($score->timestamp) || + isset($score->timestamp) && !gradebookservices::validate_iso8601_date($score->timestamp) || + (isset($score->scoreGiven) && !is_numeric($score->scoreGiven)) || + (isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) || + (!gradebookservices::is_user_gradable_in_course($contextid, $score->userId)) + ) { + throw new \Exception('Incorrect score received' . $score, 400); + } + $score->timemodified = intval($score->timestamp); + + if (!isset($score->scoreMaximum)) { + $score->scoreMaximum = 1; + } + $response->set_code(200); + $grade = \grade_grade::fetch(array('itemid' => $item->id, 'userid' => $score->userId)); + if ($grade && !empty($grade->timemodified)) { + if ($grade->timemodified >= strtotime($score->timestamp)) { + $exmsg = "Refusing score with an earlier timestamp for item " . $item->id . " and user " . $score->userId; + throw new \Exception($exmsg, 409); + } + } + if (isset($score->scoreGiven)) { + if ($score->gradingProgress != 'FullyGraded') { + $score->scoreGiven = null; + } + } + gradebookservices::save_score($item, $score, $score->userId, $typeid); + } + + /** + * get permissions from the config of the tool for that resource + * + * @param int $typeid + * + * @return array with the permissions related to this resource by the $lti_type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->ltiservice_gradesynchronization == '1') { + return array('Score.collection:post'); + } else if ($tool->ltiservice_gradesynchronization == '2') { + return array('Score.collection:post'); + } else { + return array(); + } + } + + /** + * Parse a value for custom parameter substitution variables. + * + * @param string $value String to be parsed + * + * @return string + */ + public function parse_value($value) { + global $COURSE, $CFG; + + if (strpos($value, '$Scores.url') !== false) { + require_once($CFG->libdir . '/gradelib.php'); + + $resolved = ''; + $this->params['context_id'] = $COURSE->id; + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); + $id = $cm->instance; + $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id); + if ($item && $item->items) { + $this->params['item_id'] = $item->items[0]->id; + $resolved = parent::get_endpoint(); + } + } + $value = str_replace('$Scores.url', $resolved, $value); + } + + return $value; + } +} diff --git a/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php b/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php new file mode 100644 index 0000000000000..554f52549b880 --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php @@ -0,0 +1,625 @@ +. + +/** + * This file contains a class definition for the LTI Gradebook Services + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\local\service; + +use ltiservice_gradebookservices\local\resources\lineitem; +use ltiservice_gradebookservices\local\resources\lineitems; +use ltiservice_gradebookservices\local\resources\results; +use ltiservice_gradebookservices\local\resources\scores; +use mod_lti\local\ltiservice\resource_base; +use mod_lti\local\ltiservice\service_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A service implementing LTI Gradebook Services. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradebookservices extends service_base { + + /** Internal service name */ + const SERVICE_NAME = 'ltiservice_gradebookservices'; + + /** + * Class constructor. + */ + public function __construct() { + + parent::__construct(); + $this->id = 'gradebookservices'; + $this->name = $this->get_string('servicename'); + + } + + /** + * Get the resources for this service. + * + * @return resource_base[] + */ + public function get_resources() { + + // The containers should be ordered in the array after their elements. + // Lineitems should be after lineitem. + if (empty($this->resources)) { + $this->resources = array(); + $this->resources[] = new lineitem($this); + $this->resources[] = new lineitems($this); + $this->resources[] = new results($this); + $this->resources[] = new scores($this); + } + + return $this->resources; + } + + /** + * Adds form elements for gradebook sync add/edit page. + * + * @param \MoodleQuickForm $mform Moodle quickform object definition + */ + public function get_configuration_options(&$mform) { + + $selectelementname = 'ltiservice_gradesynchronization'; + $identifier = 'grade_synchronization'; + $options = [ + $this->get_string('nevergs'), + $this->get_string('partialgs'), + $this->get_string('alwaysgs') + ]; + + $mform->addElement('select', $selectelementname, $this->get_string($identifier), $options); + $mform->setType($selectelementname, 'int'); + $mform->setDefault($selectelementname, 0); + $mform->addHelpButton($selectelementname, $identifier, self::SERVICE_NAME); + } + + /** + * Retrieves string from lang file + * + * @param string $identifier + * @return string + */ + private function get_string($identifier) { + return get_string($identifier, self::SERVICE_NAME); + } + + /** + * Return an array with the names of the parameters that the service will be saving in the configuration + * + * @return array with the names of the parameters that the service will be saving in the configuration + * + */ + public function get_configuration_parameter_names() { + return array('ltiservice_gradesynchronization'); + } + + /** + * Return an array of key/values to add to the launch parameters. + * + * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. + * @param string $courseid the course id. + * @param object $user The user id. + * @param string $typeid The tool lti type id. + * @param string $modlti The id of the lti activity. + * + * The type is passed to check the configuration + * and not return parameters for services not used. + * + * @return array of key/value pairs to add as launch parameters. + */ + public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { + global $DB; + + $launchparameters = array(); + $tool = lti_get_type_type_config($typeid); + // Only inject parameters if the service is enabled for this tool. + if (isset($tool->ltiservice_gradesynchronization)) { + if ($tool->ltiservice_gradesynchronization == '1' || $tool->ltiservice_gradesynchronization == '2') { + // Check for used in context is only needed because there is no explicit site tool - course relation. + if ($this->is_allowed_in_context($typeid, $courseid)) { + $endpoint = $this->get_service_path() . "/{$courseid}/lineitems"; + if (is_null($modlti)) { + $id = null; + } else { + $conditions = array('courseid' => $courseid, 'itemtype' => 'mod', + 'itemmodule' => 'lti', 'iteminstance' => $modlti); + + $lineitems = $DB->get_records('grade_items', $conditions); + $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti); + $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs); + if (count($lineitems) + count($lineitemsgbs) == 1) { + if ($lineitems) { + $lineitem = reset($lineitems); + $id = $lineitem->id; + } else { + $lineitemsgb = reset($lineitemsgbs); + $id = $lineitemsgb->gradeitemid; + } + } else { + $id = null; + } + } + $launchparameters['custom_lineitems_url'] = $endpoint . "?type_id={$typeid}"; + if (!is_null($id)) { + $launchparameters['custom_lineitem_url'] = $endpoint . "/{$id}/lineitem?type_id={$typeid}"; + } + } + } + } + return $launchparameters; + } + + /** + * Fetch the lineitem instances. + * + * @param string $courseid ID of course + * @param string $resourceid Resource identifier used for filtering, may be null + * @param string $ltilinkid Resource Link identifier used for filtering, may be null + * @param string $tag + * @param int $limitfrom Offset for the first line item to include in a paged set + * @param int $limitnum Maximum number of line items to include in the paged set + * @param string $typeid + * + * @return array + * @throws \Exception + */ + public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) { + global $DB; + + // Select all lti potential linetiems in site. + $params = array('courseid' => $courseid); + + $optionalfilters = ""; + if (isset($resourceid)) { + $optionalfilters .= " AND (i.idnumber = :resourceid)"; + $params['resourceid'] = $resourceid; + } + $sql = "SELECT i.* + FROM {grade_items} i + WHERE (i.courseid = :courseid) + {$optionalfilters} + ORDER BY i.id"; + $lineitems = $DB->get_records_sql($sql, $params); + + // For each one, check the gbs id, and check that toolproxy matches. If so, add the + // tag to the result and add it to a final results array. + $lineitemstoreturn = array(); + $lineitemsandtotalcount = array(); + if ($lineitems) { + foreach ($lineitems as $lineitem) { + $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id); + if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag)) + && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid))) { + if (is_null($typeid)) { + if ($this->get_tool_proxy()->id == $gbs->toolproxyid) { + array_push($lineitemstoreturn, $lineitem); + } + } else { + if ($typeid == $gbs->typeid) { + array_push($lineitemstoreturn, $lineitem); + } + } + } else if (($lineitem->itemtype == 'mod') && ($lineitem->itemmodule == 'lti') && (!isset($tag) && + (!isset($ltilinkid) || (isset($ltilinkid) && $lineitem->iteminstance == $ltilinkid)))) { + // We will need to check if the activity related belongs to our tool proxy. + $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); + if (($ltiactivity) && (isset($ltiactivity->typeid))) { + if ($ltiactivity->typeid != 0) { + $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); + } else { + $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); + if (!$tool) { + $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); + } + } + if (is_null($typeid)) { + if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) { + array_push($lineitemstoreturn, $lineitem); + } + } else { + if (($tool) && ($tool->id == $typeid)) { + array_push($lineitemstoreturn, $lineitem); + } + } + } + } + } + $lineitemsandtotalcount = array(); + array_push($lineitemsandtotalcount, count($lineitemstoreturn)); + // Return the right array based in the paging parameters limit and from. + if (($limitnum) && ($limitnum > 0)) { + $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum); + } + array_push($lineitemsandtotalcount, $lineitemstoreturn); + } + return $lineitemsandtotalcount; + } + + /** + * Fetch a lineitem instance. + * + * Returns the lineitem instance if found, otherwise false. + * + * @param string $courseid ID of course + * @param string $itemid ID of lineitem + * @param string $typeid + * + * @return \ltiservice_gradebookservices\local\resources\lineitem|bool + */ + public function get_lineitem($courseid, $itemid, $typeid) { + global $DB, $CFG; + + require_once($CFG->libdir . '/gradelib.php'); + $lineitem = \grade_item::fetch(array('id' => $itemid)); + if ($lineitem) { + $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid); + if (!$gbs) { + // We will need to check if the activity related belongs to our tool proxy. + $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance)); + if (($ltiactivity) && (isset($ltiactivity->typeid))) { + if ($ltiactivity->typeid != 0) { + $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid)); + } else { + $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid); + if (!$tool) { + $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid); + } + } + if (is_null($typeid)) { + if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) { + return false; + } + } else { + if (!(($tool) && ($tool->id == $typeid))) { + return false; + } + } + } else { + return false; + } + } + } + return $lineitem; + } + + + /** + * Set a grade item. + * + * @param object $gradeitem Grade Item record + * @param object $score Result object + * @param int $userid User ID + * + * @throws \Exception + */ + public static function save_score($gradeitem, $score, $userid) { + global $DB, $CFG; + $source = 'mod' . self::SERVICE_NAME; + if ($DB->get_record('user', array('id' => $userid)) === false) { + throw new \Exception(null, 400); + } + require_once($CFG->libdir . '/gradelib.php'); + $finalgrade = null; + $timemodified = null; + if (isset($score->scoreGiven) && $score->scoreGiven) { + $finalgrade = grade_floatval($score->scoreGiven); + $max = 1; + if (isset($score->scoreMaximum)) { + $max = $score->scoreMaximum; + } + if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) { + // Rescale to match the grade item maximum. + $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max); + } + if (isset($score->timestamp)) { + $timemodified = strtotime($score->timestamp); + } else { + $timemodified = time(); + } + } + $feedbackformat = FORMAT_MOODLE; + $feedback = null; + if (isset($score->comment) && !empty($score->comment)) { + $feedback = $score->comment; + $feedbackformat = FORMAT_PLAIN; + } + + if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) { + $grade = new \grade_grade(); + $grade->userid = $userid; + $grade->itemid = $gradeitem->id; + } + $grade->rawgrademax = $score->scoreMaximum; + $grade->timemodified = $timemodified; + $grade->feedbackformat = $feedbackformat; + $grade->feedback = $feedback; + if ($gradeitem->is_manual_item()) { + $grade->finalgrade = $finalgrade; + if (empty($grade->id)) { + $result = (bool)$grade->insert($source); + } else { + $result = $grade->update($source); + } + } else { + $grade->rawgrade = $finalgrade; + $status = \grade_update($source, $gradeitem->courseid, + $gradeitem->itemtype, $gradeitem->itemmodule, + $gradeitem->iteminstance, $gradeitem->itemnumber, + $grade); + + $result = ($status == GRADE_UPDATE_OK); + } + if (!$result) { + debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid); + throw new \Exception(null, 500); + } + + } + + /** + * Get the json object representation of the grade item + * + * @param object $item Grade Item record + * @param string $endpoint Endpoint for lineitems container request + * @param string $typeid + * + * @return object + */ + public static function item_for_json($item, $endpoint, $typeid) { + + $lineitem = new \stdClass(); + if (is_null($typeid)) { + $typeidstring = ""; + } else { + $typeidstring = "?type_id={$typeid}"; + } + $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring; + $lineitem->label = $item->itemname; + $lineitem->scoreMaximum = floatval($item->grademax); + $lineitem->resourceId = (!empty($item->idnumber)) ? $item->idnumber : ''; + $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id); + if ($gbs) { + $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : ''; + if (isset($gbs->ltilinkid)) { + $lineitem->ltiLinkId = strval($gbs->ltilinkid); + } + } else { + $lineitem->tag = ''; + if (isset($item->iteminstance)) { + $lineitem->ltiLinkId = strval($item->iteminstance); + } + } + + return $lineitem; + + } + + /** + * Get the object matching the JSON representation of the result. + * + * @param object $grade Grade record + * @param string $endpoint Endpoint for lineitem + * @param int $typeid The id of the type to include in the result url. + * + * @return object + */ + public static function result_for_json($grade, $endpoint, $typeid) { + + if (is_null($typeid)) { + $id = "{$endpoint}/results?user_id={$grade->userid}"; + } else { + $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}"; + } + $result = new \stdClass(); + $result->id = $id; + $result->userId = $grade->userid; + if (!empty($grade->finalgrade)) { + $result->resultScore = floatval($grade->finalgrade); + $result->resultMaximum = floatval($grade->rawgrademax); + if (!empty($grade->feedback)) { + $result->comment = $grade->feedback; + } + if (is_null($typeid)) { + $result->scoreOf = $endpoint; + } else { + $result->scoreOf = "{$endpoint}?type_id={$typeid}"; + } + $result->timestamp = date('c', $grade->timemodified); + } + return $result; + } + + /** + * Check if an LTI id is valid. + * + * @param string $linkid The lti id + * @param string $course The course + * @param string $toolproxy The tool proxy id + * + * @return boolean + */ + public static function check_lti_id($linkid, $course, $toolproxy) { + global $DB; + // Check if lti type is zero or not (comes from a backup). + $sqlparams1 = array(); + $sqlparams1['linkid'] = $linkid; + $sqlparams1['course'] = $course; + $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); + if ($ltiactivity->typeid == 0) { + $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); + if (!$tool) { + $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); + } + return (($tool) && ($toolproxy == $tool->toolproxyid)); + } else { + $sqlparams2 = array(); + $sqlparams2['linkid'] = $linkid; + $sqlparams2['course'] = $course; + $sqlparams2['toolproxy'] = $toolproxy; + $sql = 'SELECT lti.* + FROM {lti} lti + INNER JOIN {lti_types} typ ON lti.typeid = typ.id + WHERE lti.id = ? + AND lti.course = ? + AND typ.toolproxyid = ?'; + return $DB->record_exists_sql($sql, $sqlparams2); + } + } + + /** + * Check if an LTI id is valid when we are in a LTI 1.x case + * + * @param string $linkid The lti id + * @param string $course The course + * @param string $typeid The lti type id + * + * @return boolean + */ + public static function check_lti_1x_id($linkid, $course, $typeid) { + global $DB; + // Check if lti type is zero or not (comes from a backup). + $sqlparams1 = array(); + $sqlparams1['linkid'] = $linkid; + $sqlparams1['course'] = $course; + $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course)); + if ($ltiactivity) { + if ($ltiactivity->typeid == 0) { + $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course); + if (!$tool) { + $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course); + } + return (($tool) && ($typeid == $tool->id)); + } else { + $sqlparams2 = array(); + $sqlparams2['linkid'] = $linkid; + $sqlparams2['course'] = $course; + $sqlparams2['typeid'] = $typeid; + $sql = 'SELECT lti.* + FROM {lti} lti + INNER JOIN {lti_types} typ ON lti.typeid = typ.id + WHERE lti.id = ? + AND lti.course = ? + AND typ.id = ?'; + return $DB->record_exists_sql($sql, $sqlparams2); + } + } else { + return false; + } + } + + /** + * Deletes orphaned rows from the 'ltiservice_gradebookservices' table. + * + * Sometimes, if a gradebook entry is deleted and it was a lineitem + * the row in the table ltiservice_gradebookservices can become an orphan + * This method will clean these orphans. It will happens based on a task + * because it is not urgent and we don't want to slow the service + */ + public static function delete_orphans_ltiservice_gradebookservices_rows() { + global $DB; + + $sql = "DELETE + FROM {ltiservice_gradebookservices} + WHERE gradeitemid NOT IN (SELECT id + FROM {grade_items} gi + WHERE gi.itemtype = 'mod' + AND gi.itemmodule = 'lti')"; + $DB->execute($sql); + } + + /** + * Check if a user can be graded in a course + * + * @param int $courseid The course + * @param int $userid The user + * @return bool + */ + public static function is_user_gradable_in_course($courseid, $userid) { + global $CFG; + + $gradableuser = false; + $coursecontext = \context_course::instance($courseid); + if (is_enrolled($coursecontext, $userid, '', false)) { + $roles = get_user_roles($coursecontext, $userid); + $gradebookroles = explode(',', $CFG->gradebookroles); + foreach ($roles as $role) { + foreach ($gradebookroles as $gradebookrole) { + if ($role->roleid = $gradebookrole) { + $gradableuser = true; + } + } + } + } + + return $gradableuser; + } + + /** + * Find the right element in the ltiservice_gradebookservice table for a lineitem + * + * @param string $lineitemid The lineitem + * @return object|bool gradebookservice id or false if none + */ + public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) { + global $DB; + + if (!$lineitemid) { + return false; + } + $gradeitem = $DB->get_record('grade_items', array('id' => $lineitemid)); + if ($gradeitem) { + $gbs = $DB->get_record('ltiservice_gradebookservices', + array('gradeitemid' => $gradeitem->id, 'courseid' => $gradeitem->courseid)); + if ($gbs) { + return $gbs; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Validates specific ISO 8601 format of the timestamps. + * + * @param string $date The timestamp to check. + * @return boolean true or false if the date matches the format. + * + */ + public static function validate_iso8601_date($date) { + if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' . + '(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))' . + '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' . + '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) { + return true; + } else { + return false; + } + } +} diff --git a/mod/lti/service/gradebookservices/classes/task/cleanup_task.php b/mod/lti/service/gradebookservices/classes/task/cleanup_task.php new file mode 100644 index 0000000000000..6737bcf83e3dd --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/task/cleanup_task.php @@ -0,0 +1,58 @@ +. + +/** + * A scheduled task for gradebookservices. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace ltiservice_gradebookservices\task; + +use core\task\scheduled_task; +use ltiservice_gradebookservices\local\service\gradebookservices; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class containing the scheduled task for gradebookservices. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcleanup', 'ltiservice_gradebookservices'); + } + + /** + * Run forum cron. + */ + public function execute() { + gradebookservices::delete_orphans_ltiservice_gradebookservices_rows(); + } + +} diff --git a/mod/lti/service/gradebookservices/db/install.xml b/mod/lti/service/gradebookservices/db/install.xml new file mode 100644 index 0000000000000..73e8431b94120 --- /dev/null +++ b/mod/lti/service/gradebookservices/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/mod/lti/service/gradebookservices/db/tasks.php b/mod/lti/service/gradebookservices/db/tasks.php new file mode 100644 index 0000000000000..9515b9ea2f96c --- /dev/null +++ b/mod/lti/service/gradebookservices/db/tasks.php @@ -0,0 +1,39 @@ +. + +/** + * This file defines tasks performed by the plugin. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// List of tasks. +$tasks = array( + array( + 'classname' => 'ltiservice_gradebookservices\task\cleanup_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ) +); diff --git a/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php b/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php new file mode 100644 index 0000000000000..8d0a6ae7c6d24 --- /dev/null +++ b/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php @@ -0,0 +1,38 @@ +. + +/** + * Strings for component 'ltiservice_gradebookservices', language 'en' + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['alwaysgs'] = 'Use this service for grade sync and column management '; +$string['grade_synchronization'] = 'IMS LTI Assignment and Grade Services: '; +$string['grade_synchronization_help'] = 'Use the IMS LTI Assignment and Grade Service to synchronize the grades instead Basic Outcomes. + +* **Do not use this service** - This will use the basic outcomes features and configuration +* **Use this service for grade sync only** - The service will populate the grades in an already existing gradebook column, but it will not be able to create new columns +* **Use this service for grade sync and column management** - The service will be able to create and update gradebook columns and manage the grades. '; +$string['modulename'] = 'LTI Grades'; +$string['nevergs'] = 'Do not use this service'; +$string['partialgs'] = 'Use this service for grade sync only'; +$string['pluginname'] = 'LTI Assignment and Grade Services'; +$string['servicename'] = 'LTI Assignment and Grade Services'; +$string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup'; diff --git a/mod/lti/service/gradebookservices/tests/task_cleanup_test.php b/mod/lti/service/gradebookservices/tests/task_cleanup_test.php new file mode 100644 index 0000000000000..20881e0ef5a72 --- /dev/null +++ b/mod/lti/service/gradebookservices/tests/task_cleanup_test.php @@ -0,0 +1,103 @@ +. + +/** + * Tests cleaning up the gradebook services task. + * + * @package ltiservice_gradebookservices + * @category test + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tests cleaning up the gradebook services task. + * + * @package ltiservice_gradebookservices + * @category test + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ltiservice_gradebookservices_cleanup_task_testcase extends advanced_testcase { + + /** + * Test set up. + * + * This is executed before running any test in this file. + */ + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test the cleanup task. + */ + public function test_cleanup_task() { + global $DB; + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create a few LTI items. + $lti = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); + $lti2 = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]); + + $conditions = [ + 'courseid' => $course->id, + 'itemtype' => 'mod', + 'itemmodule' => 'lti', + 'iteminstance' => $lti->id + ]; + + // Get the grade items. + $gradeitem = $DB->get_record('grade_items', $conditions); + + $conditions['iteminstance'] = $lti2->id; + $gradeitem2 = $DB->get_record('grade_items', $conditions); + + // Insert these into the 'ltiservice_gradebookservices' table. + $data = new stdClass(); + $data->gradeitemid = $gradeitem->id; + $data->courseid = $course->id; + $DB->insert_record('ltiservice_gradebookservices', $data); + + $data->gradeitemid = $gradeitem2->id; + $DB->insert_record('ltiservice_gradebookservices', $data); + + $task = new \ltiservice_gradebookservices\task\cleanup_task(); + $task->execute(); + + // Check they both still exist. + $this->assertEquals(2, $DB->count_records('ltiservice_gradebookservices')); + + // Delete the first LTI activity. + course_delete_module($lti->cmid); + + // Run the task again. + $task = new \ltiservice_gradebookservices\task\cleanup_task(); + $task->execute(); + + // Check only the second grade item exists. + $gradebookserviceitems = $DB->get_records('ltiservice_gradebookservices'); + $this->assertCount(1, $gradebookserviceitems); + + $gradebookserviceitem = reset($gradebookserviceitems); + + $this->assertEquals($gradeitem2->id, $gradebookserviceitem->gradeitemid); + } +} diff --git a/mod/lti/service/gradebookservices/version.php b/mod/lti/service/gradebookservices/version.php new file mode 100644 index 0000000000000..6afb647969f46 --- /dev/null +++ b/mod/lti/service/gradebookservices/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version information for the ltiservice_gradebookservices service. + * + * @package ltiservice_gradebookservices + * @copyright 2017 Cengage Learning http://www.cengage.com + * @author Dirk Singels, Diego del Blanco, Claude Vervoort + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018011100; +$plugin->requires = 2018010100; +$plugin->component = 'ltiservice_gradebookservices'; diff --git a/mod/lti/service/memberships/classes/local/resources/contextmemberships.php b/mod/lti/service/memberships/classes/local/resources/contextmemberships.php index 7afb386fc0758..8536934909474 100644 --- a/mod/lti/service/memberships/classes/local/resources/contextmemberships.php +++ b/mod/lti/service/memberships/classes/local/resources/contextmemberships.php @@ -26,8 +26,9 @@ namespace ltiservice_memberships\local\resources; -use \mod_lti\local\ltiservice\service_base; +use mod_lti\local\ltiservice\resource_base; use ltiservice_memberships\local\service\memberships; +use core_availability\info_module; defined('MOODLE_INTERNAL') || die(); @@ -39,18 +40,18 @@ * @copyright 2015 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class contextmemberships extends \mod_lti\local\ltiservice\resource_base { +class contextmemberships extends resource_base { /** * Class constructor. * - * @param ltiservice_memberships\local\service\memberships $service Service instance + * @param \ltiservice_memberships\local\service\memberships $service Service instance */ public function __construct($service) { parent::__construct($service); $this->id = 'ToolProxyBindingMemberships'; - $this->template = '/{context_type}/{context_id}/bindings/{vendor_code}/{product_code}/{tool_code}/memberships'; + $this->template = '/{context_type}/{context_id}/bindings/{tool_code}/memberships'; $this->variables[] = 'ToolProxyBinding.memberships.url'; $this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+json'; $this->methods[] = 'GET'; @@ -60,23 +61,24 @@ public function __construct($service) { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { - global $CFG, $DB; + global $DB; $params = $this->parse_template(); $role = optional_param('role', '', PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); + $linkid = optional_param('rlid', '', PARAM_TEXT); + $lti = null; + $modinfo = null; + if ($limitnum <= 0) { $limitfrom = 0; } try { - if (!$this->get_service()->check_tool_proxy($params['product_code'])) { - throw new \Exception(null, 401); - } if (!($course = $DB->get_record('course', array('id' => $params['context_id']), 'id', IGNORE_MISSING))) { throw new \Exception(null, 404); } @@ -84,14 +86,33 @@ public function execute($response) { throw new \Exception(null, 404); } if (!($tool = $DB->get_record('lti_types', array('id' => $params['tool_code']), - 'toolproxyid,enabledcapability,parameter', IGNORE_MISSING))) { + 'id,toolproxyid,enabledcapability,parameter', IGNORE_MISSING))) { throw new \Exception(null, 404); } - $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $tool->toolproxyid), 'guid', IGNORE_MISSING); - if (!$toolproxy || ($toolproxy->guid !== $this->get_service()->get_tool_proxy()->guid)) { - throw new \Exception(null, 400); + if (!empty($linkid)) { + if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) { + throw new \Exception(null, 404); + } + $modinfo = get_fast_modinfo($course); + $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST); + $cm = $modinfo->get_cm($cm->id); + $modinfo = new info_module($cm); + if ($modinfo->is_available_for_all()) { + $modinfo = null; + } } - $json = memberships::get_users_json($this, $context, $course->id, $tool, $role, $limitfrom, $limitnum, null, null); + if ($tool->toolproxyid == 0) { + if (!$this->check_type($params['tool_code'], $params['context_id'], + 'ToolProxyBinding.memberships.url:get', null)) { + throw new \Exception(null, 403); + } + } else { + $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $tool->toolproxyid), 'guid', IGNORE_MISSING); + if (!$this->check_tool_proxy($toolproxy->guid, $response->get_request_data())) { + throw new \Exception(null, 403); + } + } + $json = memberships::get_users_json($this, $context, $course->id, $tool, $role, $limitfrom, $limitnum, $lti, $modinfo); $response->set_content_type($this->formats[0]); $response->set_body($json); @@ -101,6 +122,21 @@ public function execute($response) { } } + /** + * get permissions from the config of the tool for that resource + * + * @param int $typeid + * @return array with the permissions related to this resource by the $lti_type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->ltiservice_memberships == '1') { + return array('ToolProxyBinding.memberships.url:get'); + } else { + return array(); + } + } + /** * Parse a value for custom parameter substitution variables. * @@ -111,25 +147,24 @@ public function execute($response) { public function parse_value($value) { global $COURSE, $DB; - if ($COURSE->id === SITEID) { - $this->params['context_type'] = 'Group'; - } else { - $this->params['context_type'] = 'CourseSection'; - } - $this->params['context_id'] = $COURSE->id; - $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode; - $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid; - - $id = optional_param('id', 0, PARAM_INT); // Course Module ID. - if (!empty($id)) { - $cm = get_coursemodule_from_id('lti', $id, 0, false, IGNORE_MISSING); - $lti = $DB->get_record('lti', array('id' => $cm->instance), 'typeid', IGNORE_MISSING); - if ($lti && !empty($lti->typeid)) { - $this->params['tool_code'] = $lti->typeid; + if (strpos($value, '$ToolProxyBinding.memberships.url') !== false) { + if ($COURSE->id === SITEID) { + $this->params['context_type'] = 'Group'; + } else { + $this->params['context_type'] = 'CourseSection'; + } + $this->params['context_id'] = $COURSE->id; + + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, IGNORE_MISSING); + $lti = $DB->get_record('lti', array('id' => $cm->instance), 'typeid', IGNORE_MISSING); + if ($lti && !empty($lti->typeid)) { + $this->params['tool_code'] = $lti->typeid; + } } + $value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value); } - $value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value); - return $value; } diff --git a/mod/lti/service/memberships/classes/local/resources/linkmemberships.php b/mod/lti/service/memberships/classes/local/resources/linkmemberships.php index 10efc244ec3c6..621a3dd0a9045 100644 --- a/mod/lti/service/memberships/classes/local/resources/linkmemberships.php +++ b/mod/lti/service/memberships/classes/local/resources/linkmemberships.php @@ -26,27 +26,29 @@ namespace ltiservice_memberships\local\resources; -use \mod_lti\local\ltiservice\service_base; +use mod_lti\local\ltiservice\resource_base; use ltiservice_memberships\local\service\memberships; -use core_availability\info; use core_availability\info_module; defined('MOODLE_INTERNAL') || die(); /** * A resource implementing Link Memberships. + * The link membership is no longer defined in the published + * version of the LTI specification. It is replaced by the + * rlid parameter in the context membership URL. * * @package ltiservice_memberships * @since Moodle 3.0 * @copyright 2015 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class linkmemberships extends \mod_lti\local\ltiservice\resource_base { +class linkmemberships extends resource_base { /** * Class constructor. * - * @param ltiservice_memberships\local\service\memberships $service Service instance + * @param \ltiservice_memberships\local\service\memberships $service Service instance */ public function __construct($service) { @@ -62,55 +64,77 @@ public function __construct($service) { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { - global $CFG, $DB; + global $DB; $params = $this->parse_template(); $linkid = $params['link_id']; $role = optional_param('role', '', PARAM_TEXT); $limitnum = optional_param('limit', 0, PARAM_INT); $limitfrom = optional_param('from', 0, PARAM_INT); + if ($limitnum <= 0) { $limitfrom = 0; } - try { - if (empty($linkid)) { - throw new \Exception(null, 404); - } - if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) { - throw new \Exception(null, 404); + if (empty($linkid)) { + $response->set_code(404); + return; + } + if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) { + $response->set_code(404); + return; + } + $tool = $DB->get_record('lti_types', array('id' => $lti->typeid)); + if ($tool->toolproxyid == 0) { // We wil use the same permission for this and contextmembers. + if (!$this->check_type($lti->typeid, $lti->course, 'ToolProxyBinding.memberships.url:get', null)) { + $response->set_code(403); + return; } - $tool = $DB->get_record('lti_types', array('id' => $lti->typeid)); + } else { $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $tool->toolproxyid)); if (!$this->check_tool_proxy($toolproxy->guid, $response->get_request_data())) { - throw new \Exception(null, 401); - } - if (!($course = $DB->get_record('course', array('id' => $lti->course), 'id', IGNORE_MISSING))) { - throw new \Exception(null, 404); + $response->set_code(403); + return; } - if (!($context = \context_course::instance($lti->course))) { - throw new \Exception(null, 404); - } - $modinfo = get_fast_modinfo($course); - $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST); - $cm = $modinfo->get_cm($cm->id); - $info = new info_module($cm); - if ($info->is_available_for_all()) { - $info = null; - } - - $json = memberships::get_users_json($this, $context, $lti->course, $tool, $role, $limitfrom, $limitnum, $lti, $info); + } + if (!($course = $DB->get_record('course', array('id' => $lti->course), 'id', IGNORE_MISSING))) { + $response->set_code(404); + return; + } + if (!($context = \context_course::instance($lti->course))) { + $response->set_code(404); + return; + } + $modinfo = get_fast_modinfo($course); + $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST); + $cm = $modinfo->get_cm($cm->id); + $info = new info_module($cm); + if ($info->is_available_for_all()) { + $info = null; + } + $json = memberships::get_users_json($this, $context, $lti->course, $tool, $role, $limitfrom, $limitnum, $lti, $info); - $response->set_content_type($this->formats[0]); - $response->set_body($json); + $response->set_content_type($this->formats[0]); + $response->set_body($json); + } - } catch (\Exception $e) { - $response->set_code($e->getCode()); + /** + * get permissions from the config of the tool for that resource + * + * @param string $typeid + * + * @return array with the permissions related to this resource by the $lti_type or null if none. + */ + public function get_permissions($typeid) { + $tool = lti_get_type_type_config($typeid); + if ($tool->memberships == '1') { + return array('ToolProxyBinding.memberships.url:get'); + } else { + return array(); } - } /** @@ -122,13 +146,14 @@ public function execute($response) { */ public function parse_value($value) { - $id = optional_param('id', 0, PARAM_INT); // Course Module ID. - if (!empty($id)) { - $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); - $this->params['link_id'] = $cm->instance; + if (strpos($value, '$ToolProxyBinding.memberships.url') !== false) { + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); + $this->params['link_id'] = $cm->instance; + } + $value = str_replace('$LtiLink.memberships.url', parent::get_endpoint(), $value); } - $value = str_replace('$LtiLink.memberships.url', parent::get_endpoint(), $value); - return $value; } diff --git a/mod/lti/service/memberships/classes/local/service/memberships.php b/mod/lti/service/memberships/classes/local/service/memberships.php index 8066d1be30ce1..ed6a0713ac44a 100644 --- a/mod/lti/service/memberships/classes/local/service/memberships.php +++ b/mod/lti/service/memberships/classes/local/service/memberships.php @@ -23,7 +23,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - namespace ltiservice_memberships\local\service; defined('MOODLE_INTERNAL') || die(); @@ -46,6 +45,18 @@ class memberships extends \mod_lti\local\ltiservice\service_base { const CONTEXT_ROLE_LEARNER = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'; /** Capability used to identify Instructors */ const INSTRUCTOR_CAPABILITY = 'moodle/course:manageactivities'; + /** Name of LTI service component */ + const LTI_SERVICE_COMPONENT = 'ltiservice_memberships'; + /** Membership services enabled */ + const MEMBERSHIP_ENABLED = 1; + /** Always include field */ + const ALWAYS_INCLUDE_FIELD = 1; + /** Allow the instructor to decide if included */ + const DELEGATE_TO_INSTRUCTOR = 2; + /** Instructor chose to include field */ + const INSTRUCTOR_INCLUDED = 1; + /** Instructor delegated and approved for include */ + const INSTRUCTOR_DELEGATE_INCLUDED = array(self::DELEGATE_TO_INSTRUCTOR && self::INSTRUCTOR_INCLUDED); /** * Class constructor. @@ -54,7 +65,7 @@ public function __construct() { parent::__construct(); $this->id = 'memberships'; - $this->name = get_string('servicename', 'ltiservice_memberships'); + $this->name = get_string('servicename', self::LTI_SERVICE_COMPONENT); } @@ -80,17 +91,18 @@ public function get_resources() { * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param \context_course $context Course context - * @param string $id Course ID + * @param string $contextid Course ID * @param object $tool Tool instance object * @param string $role User role requested (empty if none) * @param int $limitfrom Position of first record to be returned * @param int $limitnum Maximum number of records to be returned * @param object $lti LTI instance record - * @param info_module $info Conditional availability information for LTI instance (null if context-level request) + * @param \core_availability\info_module $info Conditional availability information + * for LTI instance (null if context-level request) * - * @return array + * @return string */ - public static function get_users_json($resource, $context, $id, $tool, $role, $limitfrom, $limitnum, $lti, $info) { + public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) { $withcapability = ''; $exclude = array(); @@ -110,10 +122,9 @@ public static function get_users_json($resource, $context, $id, $tool, $role, $l $limitfrom = 0; $limitnum = 0; } - $json = self::users_to_json($resource, $users, $id, $tool, $exclude, $limitfrom, $limitnum, $lti, $info); + $json = self::users_to_json($resource, $users, $contextid, $tool, $exclude, $limitfrom, $limitnum, $lti, $info); return $json; - } /** @@ -124,7 +135,7 @@ public static function get_users_json($resource, $context, $id, $tool, $role, $l * * @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request * @param array $users Array of user records - * @param string $id Course ID + * @param string $contextid Course ID * @param object $tool Tool instance object * @param array $exclude Array of user records to be excluded from the response * @param int $limitfrom Position of first record to be returned @@ -134,82 +145,197 @@ public static function get_users_json($resource, $context, $id, $tool, $role, $l * * @return string */ - private static function users_to_json($resource, $users, $id, $tool, $exclude, $limitfrom, $limitnum, - $lti, $info) { + private static function users_to_json($resource, $users, $contextid, $tool, $exclude, $limitfrom, $limitnum, + $lti, $info) { + global $DB; + + $arrusers = [ + '@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer', + '@type' => 'Page', + '@id' => $resource->get_endpoint(), + ]; - $nextpage = 'null'; if ($limitnum > 0) { $limitfrom += $limitnum; - $nextpage = "\"{$resource->get_endpoint()}?limit={$limitnum}&from={$limitfrom}\""; + $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$limitfrom}"; + if (!is_null($lti)) { + $nextpage .= "&rlid={$lti->id}"; + } + $arrusers['nextPage'] = $nextpage; } - $json = <<< EOD -{ - "@context" : "http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer", - "@type" : "Page", - "@id" : "{$resource->get_endpoint()}", - "nextPage" : {$nextpage}, - "pageOf" : { - "@type" : "LISMembershipContainer", - "membershipSubject" : { - "@type" : "Context", - "contextId" : "{$id}", - "membership" : [ - -EOD; + + $arrusers['pageOf'] = [ + '@type' => 'LISMembershipContainer', + 'membershipSubject' => [ + '@type' => 'Context', + 'contextId' => $contextid, + 'membership' => [] + ] + ]; + $enabledcapabilities = lti_get_enabled_capabilities($tool); - $sep = ' '; + $islti2 = $tool->toolproxyid > 0; foreach ($users as $user) { - $include = !in_array($user->id, $exclude); - if ($include && !empty($info)) { - $include = $info->is_user_visible($info->get_course_module(), $user->id); + if (in_array($user->id, $exclude)) { + continue; } - if ($include) { - $member = new \stdClass(); - if (in_array('User.id', $enabledcapabilities)) { - $member->userId = $user->id; - } - if (in_array('Person.sourcedId', $enabledcapabilities)) { - $member->sourcedId = format_string($user->idnumber); - } - if (in_array('Person.name.full', $enabledcapabilities)) { - $member->name = format_string("{$user->firstname} {$user->lastname}"); - } - if (in_array('Person.name.given', $enabledcapabilities)) { - $member->givenName = format_string($user->firstname); - } - if (in_array('Person.name.family', $enabledcapabilities)) { - $member->familyName = format_string($user->lastname); - } - if (in_array('Person.email.primary', $enabledcapabilities)) { - $member->email = format_string($user->email); + if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) { + continue; + } + + $member = new \stdClass(); + $member->{"@type" } = 'LISPerson'; + $membership = new \stdClass(); + $membership->status = 'Active'; + $membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true)); + + $toolconfig = lti_get_type_type_config($tool->id); + $instanceconfig = null; + if (!is_null($lti)) { + $instanceconfig = lti_get_type_config_from_instance($lti->id); + } + $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig, + ['name' => 'lti_sendname', 'email' => 'lti_sendemailaddr']); + + $includedcapabilities = [ + 'User.id' => ['type' => 'id', + 'member.field' => 'userId', + 'source.value' => $user->id], + 'Person.sourcedId' => ['type' => 'id', + 'member.field' => 'sourcedId', + 'source.value' => format_string($user->idnumber)], + 'Person.name.full' => ['type' => 'name', + 'member.field' => 'name', + 'source.value' => format_string("{$user->firstname} {$user->lastname}")], + 'Person.name.given' => ['type' => 'name', + 'member.field' => 'givenName', + 'source.value' => format_string($user->firstname)], + 'Person.name.family' => ['type' => 'name', + 'member.field' => 'familyName', + 'source.value' => format_string($user->lastname)], + 'Person.email.primary' => ['type' => 'email', + 'member.field' => 'email', + 'source.value' => format_string($user->email)] + ]; + + if (!is_null($lti)) { + $message = new \stdClass(); + $message->message_type = 'basic-lti-launch-request'; + $conditions = array('courseid' => $contextid, 'itemtype' => 'mod', + 'itemmodule' => 'lti', 'iteminstance' => $lti->id); + + if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) { + $message->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id, + $user->id, + $lti->servicesalt, + $lti->typeid)); } - if (in_array('Result.sourcedId', $enabledcapabilities) && !empty($lti) && !empty($lti->servicesalt)) { - $member->resultSourcedId = json_encode(lti_build_sourcedid($lti->id, $user->id, $lti->servicesalt, - $lti->typeid)); + $membership->message = $message; + } + + foreach ($includedcapabilities as $capabilityname => $capability) { + if ($islti2) { + if (!in_array($capabilityname, $enabledcapabilities)) { + continue; + } + } else { + if (($capability['type'] === 'id') + || ($capability['type'] === 'name' && $isallowedlticonfig['name']) + || ($capability['type'] === 'email' && $isallowedlticonfig['email'])) { + $member->{$capability['member.field']} = $capability['source.value']; + } } - $roles = explode(',', lti_get_ims_role($user->id, null, $id, true)); + } - $membership = new \stdClass(); - $membership->status = 'Active'; - $membership->member = $member; - $membership->role = $roles; + $membership->member = $member; - $json .= $sep . json_encode($membership); - $sep = ",\n "; - } + $arrusers['pageOf']['membershipSubject']['membership'][] = $membership; + } + return json_encode($arrusers); + } + + /** + * Determines whether a user attribute may be used as part of LTI membership + * @param object $toolconfig Tool config + * @param object $instanceconfig Tool instance config + * @param array $fields Set of fields to return if allowed or not + * @return array Verification which associates an attribute with a boolean (allowed or not) + */ + private static function is_allowed_field_set($toolconfig, $instanceconfig, $fields) { + $isallowedstate = []; + foreach ($fields as $key => $field) { + $allowed = self::ALWAYS_INCLUDE_FIELD == $toolconfig->{$field}; + if (!$allowed) { + if (self::DELEGATE_TO_INSTRUCTOR == $toolconfig->{$field} && !is_null($instanceconfig)) { + $allowed = $instanceconfig->{$field} == self::INSTRUCTOR_INCLUDED; + } + } + $isallowedstate[$key] = $allowed; } + return $isallowedstate; + } - $json .= <<< EOD + /** + * Adds form elements for membership add/edit page. + * + * @param \MoodleQuickForm $mform + */ + public function get_configuration_options(&$mform) { + $elementname = 'ltiservice_memberships'; + $options = [ + get_string('notallow', self::LTI_SERVICE_COMPONENT), + get_string('allow', self::LTI_SERVICE_COMPONENT) + ]; - ] + $mform->addElement('select', $elementname, get_string($elementname, self::LTI_SERVICE_COMPONENT), $options); + $mform->setType($elementname, 'int'); + $mform->setDefault($elementname, 0); + $mform->addHelpButton($elementname, $elementname, self::LTI_SERVICE_COMPONENT); } - } -} -EOD; - return $json; + /** + * Return an array with the names of the parameters that the service will be saving in the configuration + * + * @return array with the names of the parameters that the service will be saving in the configuration + * + */ + public function get_configuration_parameter_names() { + return array(self::LTI_SERVICE_COMPONENT); + } + + /** + * Return an array of key/values to add to the launch parameters. + * + * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. + * @param string $courseid The course id. + * @param string $user The user id. + * @param string $typeid The tool lti type id. + * @param string $modlti The id of the lti activity. + * + * The type is passed to check the configuration + * and not return parameters for services not used. + * + * @return array of key/value pairs to add as launch parameters. + */ + public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) { + global $COURSE; + $launchparameters = array(); + $tool = lti_get_type_type_config($typeid); + if (isset($tool->ltiservice_memberships)) { + if ($tool->ltiservice_memberships == '1' && $this->is_used_in_context($typeid, $courseid)) { + $endpoint = $this->get_service_path(); + if ($COURSE->id === SITEID) { + $contexttype = 'Group'; + } else { + $contexttype = 'CourseSection'; + } + $launchparameters['custom_context_memberships_url'] = $endpoint . + "/{$contexttype}/{$courseid}/bindings/{$typeid}/memberships"; + } + } + return $launchparameters; } } diff --git a/mod/lti/service/memberships/lang/en/ltiservice_memberships.php b/mod/lti/service/memberships/lang/en/ltiservice_memberships.php index acb65d66a1e37..cd8d220dbe658 100644 --- a/mod/lti/service/memberships/lang/en/ltiservice_memberships.php +++ b/mod/lti/service/memberships/lang/en/ltiservice_memberships.php @@ -23,5 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['allow'] = 'Use this service to retrieve members\' information as per privacy settings'; +$string['ltiservice_memberships'] = 'IMS LTI Membership: '; +$string['ltiservice_memberships_help'] = 'Allow the tool to retrieve member\'s info from the course using the IMS LTI Membership Service. The privacy settings will apply.'; +$string['notallow'] = 'Do not use this service'; $string['pluginname'] = 'Memberships LTI Service'; $string['servicename'] = 'Memberships'; diff --git a/mod/lti/service/profile/classes/local/resources/profile.php b/mod/lti/service/profile/classes/local/resources/profile.php index e636b29efdb26..bd55ac43b8b61 100644 --- a/mod/lti/service/profile/classes/local/resources/profile.php +++ b/mod/lti/service/profile/classes/local/resources/profile.php @@ -43,7 +43,7 @@ class profile extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * - * @param ltiservice_profile\local\resources\profile $service Service instance + * @param service_base $service Service instance */ public function __construct($service) { @@ -76,10 +76,9 @@ public function get_path() { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { - global $CFG; $version = service_base::LTI_VERSION2P0; @@ -104,6 +103,7 @@ public function execute($response) { foreach ($services as $name => $location) { if (in_array($name, $serviceofferedarr)) { $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; + /** @var service_base $service */ $service = new $classname(); $service->set_tool_proxy($toolproxy); $resources = $service->get_resources(); @@ -218,9 +218,9 @@ public function get_endpoint() { * @return string */ public function parse_value($value) { - - $value = str_replace('$ToolConsumerProfile.url', $this->get_endpoint(), $value); - + if (strpos($value, '$ToolConsumerProfile.url') !== false) { + $value = str_replace('$ToolConsumerProfile.url', $this->get_endpoint(), $value); + } return $value; } diff --git a/mod/lti/service/toolsettings/classes/local/resources/contextsettings.php b/mod/lti/service/toolsettings/classes/local/resources/contextsettings.php index bbe9b2d47f342..e7c7b5c29afdf 100644 --- a/mod/lti/service/toolsettings/classes/local/resources/contextsettings.php +++ b/mod/lti/service/toolsettings/classes/local/resources/contextsettings.php @@ -26,7 +26,6 @@ namespace ltiservice_toolsettings\local\resources; -use ltiservice_toolsettings\local\resources\systemsettings; use ltiservice_toolsettings\local\service\toolsettings; defined('MOODLE_INTERNAL') || die(); @@ -44,7 +43,7 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * - * @param ltiservice_toolsettings\local\resources\contextsettings $service Service instance + * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { @@ -62,7 +61,7 @@ public function __construct($service) { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { @@ -166,16 +165,17 @@ public function execute($response) { public function parse_value($value) { global $COURSE; - if ($COURSE->format == 'site') { - $this->params['context_type'] = 'Group'; - } else { - $this->params['context_type'] = 'CourseSection'; + if (strpos($value, '$ToolProxyBinding.custom.url') !== false) { + if ($COURSE->format == 'site') { + $this->params['context_type'] = 'Group'; + } else { + $this->params['context_type'] = 'CourseSection'; + } + $this->params['context_id'] = $COURSE->id; + $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode; + $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid; + $value = str_replace('$ToolProxyBinding.custom.url', parent::get_endpoint(), $value); } - $this->params['context_id'] = $COURSE->id; - $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode; - $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid; - $value = str_replace('$ToolProxyBinding.custom.url', parent::get_endpoint(), $value); - return $value; } diff --git a/mod/lti/service/toolsettings/classes/local/resources/linksettings.php b/mod/lti/service/toolsettings/classes/local/resources/linksettings.php index 595dd576d503e..2c7700620740f 100644 --- a/mod/lti/service/toolsettings/classes/local/resources/linksettings.php +++ b/mod/lti/service/toolsettings/classes/local/resources/linksettings.php @@ -23,11 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - namespace ltiservice_toolsettings\local\resources; -use ltiservice_toolsettings\local\resources\systemsettings; -use ltiservice_toolsettings\local\resources\contextsettings; use ltiservice_toolsettings\local\service\toolsettings; defined('MOODLE_INTERNAL') || die(); @@ -45,7 +42,7 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base { /** * Class constructor. * - * @param ltiservice_toolsettings\local\resources\linksettings $service Service instance + * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { @@ -63,7 +60,7 @@ public function __construct($service) { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { global $DB, $COURSE; @@ -82,6 +79,7 @@ public function execute($response) { $systemsetting = null; $contextsetting = null; + $lti = null; if ($ok) { $ok = !empty($linkid); if ($ok) { @@ -195,13 +193,14 @@ public function execute($response) { */ public function parse_value($value) { - $id = optional_param('id', 0, PARAM_INT); // Course Module ID. - if (!empty($id)) { - $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); - $this->params['link_id'] = $cm->instance; + if (strpos($value, '$LtiLink.custom.url') !== false) { + $id = optional_param('id', 0, PARAM_INT); // Course Module ID. + if (!empty($id)) { + $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST); + $this->params['link_id'] = $cm->instance; + } + $value = str_replace('$LtiLink.custom.url', parent::get_endpoint(), $value); } - $value = str_replace('$LtiLink.custom.url', parent::get_endpoint(), $value); - return $value; } diff --git a/mod/lti/service/toolsettings/classes/local/resources/systemsettings.php b/mod/lti/service/toolsettings/classes/local/resources/systemsettings.php index 28268413565ef..b6be719216ad4 100644 --- a/mod/lti/service/toolsettings/classes/local/resources/systemsettings.php +++ b/mod/lti/service/toolsettings/classes/local/resources/systemsettings.php @@ -27,6 +27,7 @@ namespace ltiservice_toolsettings\local\resources; use ltiservice_toolsettings\local\service\toolsettings; +use mod_lti\local\ltiservice\resource_base; defined('MOODLE_INTERNAL') || die(); @@ -38,12 +39,12 @@ * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class systemsettings extends \mod_lti\local\ltiservice\resource_base { +class systemsettings extends resource_base { /** * Class constructor. * - * @param ltiservice_toolsettings\local\service\toolsettings $service Service instance + * @param \mod_lti\local\ltiservice\service_base $service Service instance */ public function __construct($service) { @@ -61,7 +62,7 @@ public function __construct($service) { /** * Execute the request for this resource. * - * @param mod_lti\local\ltiservice\response $response Response object for this request. + * @param \mod_lti\local\ltiservice\response $response Response object for this request. */ public function execute($response) { @@ -143,9 +144,9 @@ public function execute($response) { * @return string */ public function parse_value($value) { - - $value = str_replace('$ToolProxy.custom.url', parent::get_endpoint(), $value); - + if (strpos($value, '$ToolProxy.custom.url') !== false) { + $value = str_replace('$ToolProxy.custom.url', parent::get_endpoint(), $value); + } return $value; }