MDL-60416 mod_lti: Introduced LTI Advantage

A huge thanks goes to 'Diego del Blanco' and
'Chris Lawson' for contributing much of their
code and time to this project, and to Cengage
for sponsoring it.

Much appreciated!
This commit is contained in:
Claude Vervoort 2018-03-15 13:01:16 -04:00 committed by Mark Nelson
parent 1c217fa9ee
commit c02324d96a
27 changed files with 3134 additions and 215 deletions

View File

@ -87,6 +87,23 @@ abstract class restore_subplugin {
}
}
/**
* 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

View File

@ -28,6 +28,7 @@ namespace mod_lti\local\ltiservice;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
@ -41,7 +42,16 @@ require_once($CFG->dirroot . '/mod/lti/locallib.php');
*/
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 @@ abstract class resource_base {
/**
* Get the resource's service.
*
* @return mod_lti\local\ltiservice\service_base
* @return mixed
*/
public function get_service() {
@ -190,7 +200,7 @@ abstract class resource_base {
/**
* 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 @@ abstract class resource_base {
}
}
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 @@ abstract class resource_base {
}
/**
* 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.
*

View File

@ -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 @@ class response {
500 => 'Internal Server Error',
501 => 'Not Implemented'
);
$this->additionalheaders = array();
}
@ -202,11 +205,23 @@ class response {
$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");

View File

@ -28,11 +28,13 @@ namespace mod_lti\local\ltiservice;
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 @@ abstract class service_base {
/**
* 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 @@ abstract class service_base {
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;
}
/**

View File

@ -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 @@ class mod_lti_edit_types_form extends moodleform{
$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 @@ class mod_lti_edit_types_form extends moodleform{
$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 @@ class mod_lti_edit_types_form extends moodleform{
}
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);
}
}
}

View File

@ -510,9 +510,10 @@ A base URL of *quiz.tool.com* would match the following:
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}';

View File

@ -53,6 +53,7 @@ defined('MOODLE_INTERNAL') || die;
// 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 @@ define('LTI_VERSION_2', 'LTI-2p0');
* @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 @@ EOD;
/**
* 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 = "<form action=\"" . $endpoint .
@ -2607,7 +2670,7 @@ function lti_get_services() {
*
* @param string $servicename Name of service
*
* @return mod_lti\local\ltiservice\service_base Service
* @return bool|\mod_lti\local\ltiservice\service_base Service
*/
function lti_get_service_by_name($servicename) {
@ -2624,7 +2687,7 @@ function lti_get_service_by_name($servicename) {
/**
* Finds a service by id
*
* @param array $services Array of services
* @param \mod_lti\local\ltiservice\service_base[] $services Array of services
* @param string $resourceid ID of resource
*
* @return mod_lti\local\ltiservice\service_base Service
@ -2742,15 +2805,14 @@ function get_tool_proxy_edit_url(stdClass $proxy) {
*
* @param stdClass $type The tool type
*
* @return string|void The url to the course of the tool type, void if it is a site wide type
* @return string The url to the course of the tool type, void if it is a site wide type
*/
function get_tool_type_course_url(stdClass $type) {
if ($type->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;

View File

@ -0,0 +1,137 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
}
}

View File

@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
}
}
}
}

View File

@ -0,0 +1,356 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
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);
return;
}
break;
case self::HTTP_PUT:
if (!$this->check_type($typeid, $contextid, 'LineItem.item:put', $response->get_request_data())) {
$response->set_code(403);
return;
}
break;
case self::HTTP_DELETE:
if (!$this->check_type($typeid, $contextid, 'LineItem.item:delete', $response->get_request_data())) {
$response->set_code(403);
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);
return;
}
if (!$DB->record_exists('course', array('id' => $contextid))) {
$response->set_code(404);
return;
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
$response->set_code(404);
return;
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
$response->set_code(403);
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);
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;
}
}

View File

@ -0,0 +1,364 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
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);
return;
}
break;
case self::HTTP_POST:
if (!$this->check_type($typeid, $contextid, 'LineItem.collection:post', $response->get_request_data())) {
$response->set_code(403);
return;
}
break;
default: // Should not be possible.
$response->set_code(405);
return;
}
}
if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_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);
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);
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 = '';
$nextpage = '';
$prevpage = '';
$lastpage = '';
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;
if (is_null($typeid)) {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitfrom;
}
$firstpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=0";
$canonicalpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitcurrent;
$lastpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitlast;
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitprev;
}
} else {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitfrom;
}
$firstpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=0";
$canonicalpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitcurrent;
$lastpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitlast;
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitprev;
}
}
if (isset($resourceid)) {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage .= "&resource_id={$resourceid}";
}
$firstpage .= "&resource_id={$resourceid}";
$canonicalpage .= "&resource_id={$resourceid}";
$lastpage .= "&resource_id={$resourceid}";
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage .= "&resource_id={$resourceid}";
}
}
if (isset($ltilinkid)) {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage .= "&lti_link_id={$ltilinkid}";
}
$firstpage .= "&lti_link_id={$ltilinkid}";
$canonicalpage .= "&lti_link_id={$ltilinkid}";
$lastpage .= "&lti_link_id={$ltilinkid}";
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage .= "&lti_link_id={$ltilinkid}";
}
}
if (isset($tag)) {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage .= "&tag={$tag}";
}
$firstpage .= "&tag={$tag}";
$canonicalpage .= "&tag={$tag}";
$lastpage .= "&tag={v}";
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage .= "&tag={$tag}";
}
}
}
$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 . '>; rel=“first”';
if (!(is_null($prevpage))) {
$links .= ', <' . $prevpage . '>; rel=“prev”';
}
$links .= ', <' . $canonicalpage. '>; rel=“canonical”';
if (!(is_null($nextpage))) {
$links .= ', <' . $nextpage . '>; rel=“next”';
}
$links .= ', <' . $lastpage . '>; 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;
}
}

View File

@ -0,0 +1,289 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
return;
}
} else {
if (!$this->check_type($typeid, $contextid, 'Result.collection:get', $response->get_request_data())) {
$response->set_code(403);
return;
}
}
if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
$response->set_code(400);
return;
}
if (!$DB->record_exists('course', array('id' => $contextid))) {
$response->set_code(404);
return;
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
$response->set_code(404);
return;
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
$response->set_code(403);
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);
return;
}
} else {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
$typeid))) {
$response->set_code(403);
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);
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 = '';
$nextpage = '';
$prevpage = '';
$lastpage = '';
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;
if (is_null($typeid)) {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitfrom;
}
$firstpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=0";
$canonicalpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitcurrent;
$lastpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitlast;
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = $this->get_endpoint() . "?limit=" . $limitnum . "&from=" . $limitprev;
}
} else {
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitfrom;
}
$firstpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=0";
$canonicalpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&from=" . $limitcurrent;
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = $this->get_endpoint() . "?type_id=" . $typeid . "&limit=" . $limitnum . "&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 . '>; rel=“first”';
if (!(is_null($prevpage))) {
$links .= ', <' . $prevpage . '>; rel=“prev”';
}
$links .= ', <' . $canonicalpage. '>; rel=“canonical”';
if (!(is_null($nextpage))) {
$links .= ', <' . $nextpage . '>; rel=“next”';
}
$links .= ', <' . $lastpage . '>; 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;
}
}

View File

@ -0,0 +1,261 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
return;
case 'POST':
if (!$this->check_type($typeid, $contextid, 'Score.collection:post', $response->get_request_data())) {
$response->set_code(401);
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);
return;
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
$response->set_code(404);
return;
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
$response->set_code(403);
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);
return;
}
} else {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
$typeid))) {
$response->set_code(403);
return;
}
}
}
$json = '[]';
require_once($CFG->libdir.'/gradelib.php');
switch ($response->get_request_method()) {
case 'GET':
$response->set_code(405);
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);
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;
}
}

View File

@ -0,0 +1,625 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
}
}
}

View File

@ -0,0 +1,58 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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();
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/lti/service/gradebookservices/db" VERSION="20150915" COMMENT="XMLDB file for Moodle mod/lti/service/gradebookservices"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="ltiservice_gradebookservices" COMMENT="This file records the grade items created by the LTI Gradebook Services service">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="gradeitemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the gradeItem related."/>
<FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the course related."/>
<FIELD NAME="toolproxyid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="ID of the Tool Proxy instance."/>
<FIELD NAME="typeid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="ID of the LTI Type if not Proxy."/>
<FIELD NAME="baseurl" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Lineitem URL that will be returned to the Tool provider"/>
<FIELD NAME="ltilinkid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="ID of the LTI element related with this lineitem."/>
<FIELD NAME="tag" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Tag type specified for the line item"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="ltilinkid" TYPE="foreign" FIELDS="ltilinkid" REFTABLE="lti" REFFIELDS="id" COMMENT="The ID of the LTI element related with this lineitem."/>
<KEY NAME="itemnumbercourse" TYPE="foreign" FIELDS="gradeitemid,courseid" REFTABLE="grade_items" REFFIELDS="id,courseid" COMMENT="The row in gradeitems"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>

View File

@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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' => '*'
)
);

View File

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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';

View File

@ -0,0 +1,30 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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';

View File

@ -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 @@ defined('MOODLE_INTERNAL') || die();
* @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 @@ class contextmemberships extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 @@ class contextmemberships extends \mod_lti\local\ltiservice\resource_base {
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 @@ class contextmemberships extends \mod_lti\local\ltiservice\resource_base {
}
}
/**
* 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 @@ class contextmemberships extends \mod_lti\local\ltiservice\resource_base {
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';
}
}
$value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value);
$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);
}
return $value;
}

View File

@ -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 @@ class linkmemberships extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 (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;
}
if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) {
throw new \Exception(null, 404);
}
$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);
$response->set_code(403);
return;
}
if (!($course = $DB->get_record('course', array('id' => $lti->course), 'id', IGNORE_MISSING))) {
throw new \Exception(null, 404);
}
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);
$response->set_content_type($this->formats[0]);
$response->set_body($json);
} catch (\Exception $e) {
$response->set_code($e->getCode());
}
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);
}
/**
* 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 @@ class linkmemberships extends \mod_lti\local\ltiservice\resource_base {
*/
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;
}

View File

@ -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 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
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 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
*
* @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 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
$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 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
*
* @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,70 +145,115 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
*
* @return string
*/
private static function users_to_json($resource, $users, $id, $tool, $exclude, $limitfrom, $limitnum,
$lti, $info) {
$nextpage = 'null';
if ($limitnum > 0) {
$limitfrom += $limitnum;
$nextpage = "\"{$resource->get_endpoint()}?limit={$limitnum}&amp;from={$limitfrom}\"";
}
private static function users_to_json($resource, $users, $contextid, $tool, $exclude, $limitfrom, $limitnum,
$lti, $info) {
global $DB;
$json = <<< EOD
{
"@context" : "http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer",
"@type" : "Page",
"@id" : "{$resource->get_endpoint()}",
"nextPage" : {$nextpage},
EOD;
if ($limitnum > 0) {
$limitfrom += $limitnum;
$nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$limitfrom}";
if (!is_null($lti)) {
$nextpage .= "&rlid={$lti->id}";
}
$json .= <<< EOD
"nextPage" : "{$nextpage}",
EOD;
}
$json .= <<< EOD
"pageOf" : {
"@type" : "LISMembershipContainer",
"membershipSubject" : {
"@type" : "Context",
"contextId" : "{$id}",
"contextId" : "{$contextid}",
"membership" : [
EOD;
$enabledcapabilities = lti_get_enabled_capabilities($tool);
$islti2 = $tool->toolproxyid > 0;
$sep = ' ';
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 (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));
}
$roles = explode(',', lti_get_ims_role($user->id, null, $id, true));
$membership = new \stdClass();
$membership->status = 'Active';
$membership->member = $member;
$membership->role = $roles;
$json .= $sep . json_encode($membership);
$sep = ",\n ";
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));
}
$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'];
}
}
}
$membership->member = $member;
$json .= $sep . json_encode($membership);
$sep = ",\n ";
}
$json .= <<< EOD
@ -212,4 +268,87 @@ EOD;
}
/**
* 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;
}
/**
* 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);
}
/**
* 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;
}
}

View File

@ -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';

View File

@ -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 @@ class profile extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 @@ class profile extends \mod_lti\local\ltiservice\resource_base {
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 @@ EOD;
* @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;
}

View File

@ -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 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base {
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;
}

View File

@ -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 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
$systemsetting = null;
$contextsetting = null;
$lti = null;
if ($ok) {
$ok = !empty($linkid);
if ($ok) {
@ -195,13 +193,14 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
*/
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;
}

View File

@ -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 @@ defined('MOODLE_INTERNAL') || die();
* @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 @@ class systemsettings extends \mod_lti\local\ltiservice\resource_base {
/**
* 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 @@ class systemsettings extends \mod_lti\local\ltiservice\resource_base {
* @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;
}