first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,75 @@
<?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 Basic Outcomes resource
*
* @package ltiservice_basicoutcomes
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_basicoutcomes\local\resources;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing the Basic Outcomes service.
*
* @package ltiservice_basicoutcomes
* @since Moodle 3.7
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class basicoutcomes extends \mod_lti\local\ltiservice\resource_base {
/**
* Class constructor.
*
* @param \mod_lti\local\ltiservice\service_base $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'Outcomes.LTI1';
$this->template = '';
$this->formats[] = 'application/vnd.ims.lti.v1.outcome+xml';
$this->methods[] = 'POST';
}
/**
* Get the resource fully qualified endpoint.
*
* @return string
*/
public function get_endpoint() {
$url = new \moodle_url('/mod/lti/service.php');
return $url->out(false);
}
/**
* Execute the request for this resource.
*
* @param \mod_lti\local\ltiservice\response $response Response object for this request.
*/
public function execute($response) {
// Should never be called as the endpoint sends requests to the LTI 1 service endpoint.
}
}
@@ -0,0 +1,116 @@
<?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 Basic Outcomes service
*
* @package ltiservice_basicoutcomes
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_basicoutcomes\local\service;
defined('MOODLE_INTERNAL') || die();
/**
* A service implementing Basic Outcomes.
*
* @package ltiservice_basicoutcomes
* @since Moodle 3.7
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class basicoutcomes extends \mod_lti\local\ltiservice\service_base {
/** Scope for accessing the service */
const SCOPE_BASIC_OUTCOMES = 'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome';
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'basicoutcomes';
$this->name = 'Basic Outcomes';
}
/**
* Get the resources for this service.
*
* @return array
*/
public function get_resources() {
if (empty($this->resources)) {
$this->resources = array();
$this->resources[] = new \ltiservice_basicoutcomes\local\resources\basicoutcomes($this);
}
return $this->resources;
}
/**
* Get the scope(s) permitted for the tool relevant to this service.
*
* @return array
*/
public function get_permitted_scopes() {
$scopes = array();
if (!isset($this->get_typeconfig()['acceptgrades']) || ($this->get_typeconfig()['acceptgrades'] != LTI_SETTING_NEVER)) {
$scopes[] = self::SCOPE_BASIC_OUTCOMES;
}
return $scopes;
}
/**
* Get the scope(s) permitted for the tool relevant to this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_BASIC_OUTCOMES];
}
/**
* Return an array of key/claim mapping allowing LTI 1.1 custom parameters
* to be transformed to LTI 1.3 claims.
*
* @return array Key/value pairs of params to claim mapping.
*/
public function get_jwt_claim_mappings(): array {
return [
'lis_outcome_service_url' => [
'suffix' => 'bo',
'group' => 'basicoutcome',
'claim' => 'lis_outcome_service_url',
'isarray' => false
],
'lis_result_sourcedid' => [
'suffix' => 'bo',
'group' => 'basicoutcome',
'claim' => 'lis_result_sourcedid',
'isarray' => false
]
];
}
}
@@ -0,0 +1,110 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_basicoutcomes.
*
* @package ltiservice_basicoutcomes
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_basicoutcomes\privacy;
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_basicoutcomes.
*
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->link_external_location('External LTI provider.', [
'userid' => 'privacy:metadata:userid',
'grade' => 'privacy:metadata:grade',
], 'privacy:metadata:externalpurpose');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all user data which matches the specified context.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
}
@@ -0,0 +1,32 @@
<?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_basicoutcomes', language 'en'
*
* @package ltiservice_basicoutcomes
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['allow'] = 'Use this service to accept grades from the tool';
$string['ltiservice_basicoutcomes'] = 'Basic Outcomes';
$string['ltiservice_basicoutcomes_help'] = 'Allow the tool to save and retrieve its grades to a gradebook column associated with each link.';
$string['notallow'] = 'Do not use this service';
$string['pluginname'] = 'Basic Outcomes Service';
$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
$string['privacy:metadata:grade'] = 'The tool\'s grades of the user using the LTI consumer.';
$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
+31
View File
@@ -0,0 +1,31 @@
<?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_basicoutcomes service.
*
* @package ltiservice_basicoutcomes
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_basicoutcomes';
@@ -0,0 +1,140 @@
<?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',
'resourceid',
'tag',
'vendorcode',
'guid',
'subreviewurl',
'subreviewparams'
)
);
// 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;
}
}
@@ -0,0 +1,246 @@
<?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) {
if ($data->ltilinkid != $this->get_old_parentid('lti')) {
// This is a linked item, but not for the current lti link, so skip it.
return;
}
$ltilinkid = $this->get_new_parentid('lti');
} else {
$ltilinkid = null;
}
$resourceid = null;
if (property_exists( $data, 'resourceid' )) {
$resourceid = $data->resourceid;
}
// 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,
'resourceid' => $resourceid,
'tag' => $data->tag,
'subreviewparams' => $data->subreviewparams ?? '',
'subreviewurl' => $data->subreviewurl ?? ''
));
$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;
if (!isset($gbs->resourceid)) {
// Before 3.9 resourceid was stored in grade_item->idnumber.
$gbs->resourceid = $DB->get_field_select('grade_items', 'idnumber', "id=:id", ['id' => $newgradeitemid]);
}
$DB->update_record('ltiservice_gradebookservices', $gbs);
}
}
// Pre 3.9 backups did not include a gradebookservices record. Adding one here if missing for the restored instance.
$gi = $DB->get_record('grade_items', array('itemtype' => 'mod', 'itemmodule' => 'lti', 'courseid' => $courseid,
'iteminstance' => $this->task->get_activityid()));
if ($gi) {
$gbs = $DB->get_records('ltiservice_gradebookservices', ['gradeitemid' => $gi->id]);
if (empty($gbs)) {
// The currently restored LTI link has a grade item but no gbs, so let's create a gbs entry.
if ($instance = $DB->get_record('lti', array('id' => $gi->iteminstance))) {
if ($tool = lti_get_instance_type($instance)) {
$DB->insert_record('ltiservice_gradebookservices', (object) array(
'gradeitemid' => $gi->id,
'courseid' => $courseid,
'toolproxyid' => $tool->toolproxyid,
'ltilinkid' => $gi->iteminstance,
'typeid' => $tool->id,
'baseurl' => $tool->baseurl,
'resourceid' => $gi->idnumber
));
}
}
}
}
}
}
@@ -0,0 +1,355 @@
<?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'];
$isget = $response->get_request_method() === self::HTTP_GET;
// 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_INT);
$scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM);
if ($response->get_request_method() === self::HTTP_GET) {
$scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
}
try {
if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) {
throw new \Exception(null, 401);
}
$typeid = $this->get_service()->get_type()->id;
if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
}
if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
throw new \Exception('Not allowed in context', 403);
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
throw new \Exception('Line item does not exist', 404);
}
require_once($CFG->libdir.'/gradelib.php');
switch ($response->get_request_method()) {
case self::HTTP_GET:
$this->get_request($response, $item, $typeid);
break;
case self::HTTP_PUT:
$json = $this->process_put_request($response->get_request_data(), $item, $typeid);
$response->set_body($json);
$response->set_code(200);
break;
case self::HTTP_DELETE:
$this->process_delete_request($item);
$response->set_code(204);
break;
}
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
}
/**
* 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);
}
if ($gbs) {
$resourceid = (isset($json->resourceId)) ? $json->resourceId : '';
$tag = (isset($json->tag)) ? $json->tag : '';
if ($gbs->tag !== $tag || $gbs->resourceid !== $resourceid) {
$upgradegradebookservices = true;
}
$gbs->tag = $tag;
$gbs->resourceid = $resourceid;
$incomingurl = null;
$incomingparams = null;
if (isset($json->submissionReview)) {
$incomingurl = $json->submissionReview->url ?? 'DEFAULT';
if (isset($json->submissionReview->custom)) {
$incomingparams = params_to_string($json->submissionReview->custom);
}
}
if ($gbs->subreviewurl ?? null !== $incomingurl || $gbs->subreviewparams ?? null !== $incomingparams) {
$upgradegradebookservices = true;
$gbs->subreviewurl = $incomingurl;
$gbs->subreviewparams = $incomingparams;
}
}
$ltilinkid = null;
if (isset($json->resourceLinkId)) {
if (is_numeric($json->resourceLinkId)) {
$ltilinkid = $json->resourceLinkId;
if ($gbs) {
if (intval($gbs->ltilinkid) !== intval($json->resourceLinkId)) {
$gbs->ltilinkid = $json->resourceLinkId;
$upgradegradebookservices = true;
}
} else {
if (intval($item->iteminstance) !== intval($json->resourceLinkId)) {
$item->iteminstance = intval($json->resourceLinkId);
$updategradeitem = true;
}
}
} else {
throw new \Exception(null, 400);
}
} else 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,
'resourceid' => $resourceid,
'tag' => $gbs->tag,
'subreviewurl' => $gbs->subreviewurl,
'subreviewparams' => $gbs->subreviewparams
));
}
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);
}
}
}
/**
* 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;
if ($tool = $this->get_service()->get_type()) {
$this->params['tool_code'] = $tool->id;
}
$id = optional_param('id', 0, PARAM_INT); // Course Module ID.
if (empty($id)) {
$hint = optional_param('lti_message_hint', "", PARAM_TEXT);
if ($hint) {
$hintdec = json_decode($hint);
if (isset($hintdec->cmid)) {
$id = $hintdec->cmid;
}
}
}
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();
$resolved .= "?type_id={$tool->id}";
}
}
$value = str_replace('$LineItem.url', $resolved, $value);
}
return $value;
}
}
@@ -0,0 +1,302 @@
<?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 then we are in LTI 2.
$typeid = optional_param('type_id', null, PARAM_INT);
$scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM);
if ($response->get_request_method() === self::HTTP_GET) {
$scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
}
try {
if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) {
throw new \Exception(null, 401);
}
$typeid = $this->get_service()->get_type()->id;
if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
(!empty($contenttype) && !in_array($contenttype, $this->formats))) {
throw new \Exception('No context or unsupported content type', 400);
}
if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
}
if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
throw new \Exception('Not allowed in context', 403);
}
if ($response->get_request_method() !== self::HTTP_POST) {
$resourceid = optional_param('resource_id', null, PARAM_TEXT);
$ltilinkid = optional_param('resource_link_id', null, PARAM_TEXT);
if (is_null($ltilinkid)) {
$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]);
} else {
$json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid);
$response->set_code(201);
$response->set_content_type($this->formats[1]);
}
$response->set_body($json);
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
}
/**
* Generate the JSON for a GET request.
*
* @param array $items Array of lineitems
* @param string $resourceid Resource identifier used for filtering, may be null
* @param string $ltilinkid Resource Link identifier used for filtering, may be null
* @param string $tag Tag identifier used for filtering, may be null
* @param int $limitfrom Offset of the first line item to return
* @param int $limitnum Maximum number of line items to return, ignored if zero or less
* @param int $totalcount Number of total lineitems before filtering for paging
* @param int $typeid Maximum number of line items to return, ignored if zero or less
* @param \mod_lti\local\ltiservice\response $response
* @return string
*/
private function get_json_for_get_request($items, $resourceid, $ltilinkid,
$tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) {
$firstpage = null;
$nextpage = null;
$prevpage = null;
$lastpage = null;
if (isset($limitnum) && $limitnum > 0) {
if ($limitfrom >= $totalcount || $limitfrom < 0) {
$outofrange = true;
} else {
$outofrange = false;
}
$limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0;
$limitcurrent = $limitfrom;
$limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0;
$limitfrom += $limitnum;
$baseurl = new \moodle_url($this->get_endpoint());
if (isset($resourceid)) {
$baseurl->param('resource_id', $resourceid);
}
if (isset($ltilinkid)) {
$baseurl->param('resource_link_id', $ltilinkid);
}
if (isset($tag)) {
$baseurl->param('tag', $tag);
}
if (is_null($typeid)) {
$baseurl->param('limit', $limitnum);
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
}
$firstpage = new \moodle_url($baseurl, ['from' => 0]);
$canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
$lastpage = new \moodle_url($baseurl, ['from' > $limitlast]);
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
}
} else {
$baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]);
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
}
$firstpage = new \moodle_url($baseurl, ['from' => 0]);
$canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
$lastpage = new \moodle_url($baseurl, ['from' => $limitlast]);
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
}
}
}
$jsonitems = [];
$endpoint = parent::get_endpoint();
foreach ($items as $item) {
array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid));
}
if (isset($canonicalpage) && ($canonicalpage)) {
$links = 'Link: <' . $firstpage->out() . '>; rel=“first”';
if (!is_null($prevpage)) {
$links .= ', <' . $prevpage->out() . '>; rel=“prev”';
}
$links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”';
if (!is_null($nextpage)) {
$links .= ', <' . $nextpage->out() . '>; rel=“next”';
}
$links .= ', <' . $lastpage->out() . '>; rel=“last”';
$response->add_additional_header($links);
}
return json_encode($jsonitems);
}
/**
* Generate the JSON for a POST request.
*
* @param string $body POST body
* @param string $contextid Course ID
* @param string $typeid
*
* @return string
* @throws \Exception
*/
private function get_json_for_post_request($body, $contextid, $typeid) {
global $CFG, $DB;
$json = json_decode($body);
if (empty($json) ||
!isset($json->scoreMaximum) ||
!isset($json->label)) {
throw new \Exception('No label or Score Maximum', 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->resourceLinkId)) ? $json->resourceLinkId : null;
if ($ltilinkid == null) {
$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;
}
$gradebookservices = new gradebookservices();
$id = $gradebookservices->add_standalone_lineitem($contextid, $json->label,
$max, $baseurl, $ltilinkid, $resourceid, $tag, $typeid, $toolproxyid);
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);
}
/**
* 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;
$query = '';
if (($tool = $this->get_service()->get_type())) {
$query = "?type_id={$tool->id}";
}
$value = str_replace('$LineItems.url', parent::get_endpoint() . $query, $value);
}
return $value;
}
}
@@ -0,0 +1,272 @@
<?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 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 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() === self::HTTP_GET;
// 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_INT);
$scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
try {
if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
throw new \Exception(null, 401);
}
$typeid = $this->get_service()->get_type()->id;
if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
}
if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
throw new \Exception('Not allowed in context', 403);
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
throw new \Exception('Line item does not exist', 404);
}
$gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
$ltilinkid = null;
if (isset($item->iteminstance)) {
$ltilinkid = $item->iteminstance;
} else if ($gbs && isset($gbs->ltilinkid)) {
$ltilinkid = $gbs->ltilinkid;
}
if ($ltilinkid != null) {
if (is_null($typeid)) {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
$this->get_service()->get_tool_proxy()->id))) {
$response->set_code(403);
$response->set_reason("Invalid LTI id supplied.");
return;
}
} else {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
$typeid))) {
$response->set_code(403);
$response->set_reason("Invalid LTI id supplied.");
return;
}
}
}
require_once($CFG->libdir.'/gradelib.php');
switch ($response->get_request_method()) {
case 'GET':
$useridfilter = optional_param('user_id', 0, PARAM_INT);
$limitnum = optional_param('limit', 0, PARAM_INT);
$limitfrom = optional_param('from', 0, PARAM_INT);
$typeid = optional_param('type_id', null, PARAM_TEXT);
$json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum,
$useridfilter, $typeid, $response);
$response->set_content_type($this->formats[0]);
$response->set_body($json);
break;
default: // Should not be possible.
$response->set_code(405);
$response->set_reason("Invalid request method specified.");
return;
}
$response->set_body($json);
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
}
/**
* Generate the JSON for a GET request.
*
* @param int $itemid Grade item instance ID
* @param int $limitfrom Offset for the first result to include in this paged set
* @param int $limitnum Maximum number of results to include in the response, ignored if zero
* @param int $useridfilter The user id to filter the results.
* @param int $typeid Lti tool typeid (or null)
* @param \mod_lti\local\ltiservice\response $response The response element needed to add a header.
*
* @return string
*/
private function get_json_for_get_request($itemid, $limitfrom, $limitnum, $useridfilter, $typeid, $response) {
if ($useridfilter > 0) {
$grades = \grade_grade::fetch_all(array('itemid' => $itemid, 'userid' => $useridfilter));
} else {
$grades = \grade_grade::fetch_all(array('itemid' => $itemid));
}
$firstpage = null;
$nextpage = null;
$prevpage = null;
$lastpage = null;
if ($grades && isset($limitnum) && $limitnum > 0) {
// Since we only display grades that have been modified, we need to filter first in order to support
// paging.
$resultgrades = array_filter($grades, function ($grade) {
return !empty($grade->timemodified);
});
// We save the total count to calculate the last page.
$totalcount = count($resultgrades);
// We slice to the requested item offset to insure proper item is always first, and we always return
// first pageset of any remaining items.
$grades = array_slice($resultgrades, $limitfrom);
if (count($grades) > 0) {
$pagedgrades = array_chunk($grades, $limitnum);
$pageset = 0;
$grades = $pagedgrades[$pageset];
}
if ($limitfrom >= $totalcount || $limitfrom < 0) {
$outofrange = true;
} else {
$outofrange = false;
}
$limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0;
$limitcurrent = $limitfrom;
$limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0;
$limitfrom += $limitnum;
$baseurl = new \moodle_url($this->get_endpoint());
if (is_null($typeid)) {
$baseurl->param('limit', $limitnum);
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
}
$firstpage = new \moodle_url($baseurl, ['from' => 0]);
$canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
$lastpage = new \moodle_url($baseurl, ['from' => $limitlast]);
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
}
} else {
$baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]);
if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
$nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
}
$firstpage = new \moodle_url($baseurl, ['from' => 0]);
$canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
if (($limitcurrent > 0) && (!$outofrange)) {
$prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
}
}
}
$jsonresults = [];
$lineitem = new lineitem($this->get_service());
$endpoint = $lineitem->get_endpoint();
if ($grades) {
foreach ($grades as $grade) {
if (!empty($grade->timemodified)) {
array_push($jsonresults, gradebookservices::result_for_json($grade, $endpoint, $typeid));
}
}
}
if (isset($canonicalpage) && ($canonicalpage)) {
$links = 'Link: <' . $firstpage->out() . '>; rel=“first”';
if (!is_null($prevpage)) {
$links .= ', <' . $prevpage->out() . '>; rel=“prev”';
}
$links .= ', <' . $canonicalpage->out() . '>; rel=“canonical”';
if (!is_null($nextpage)) {
$links .= ', <' . $nextpage->out() . '>; rel=“next”';
}
$links .= ', <' . $lastpage->out() . '>; rel=“last”';
$response->add_additional_header($links);
}
return json_encode($jsonresults);
}
/**
* 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;
}
}
@@ -0,0 +1,239 @@
<?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);
$scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_SCORE;
try {
if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
throw new \Exception(null, 401);
}
$typeid = $this->get_service()->get_type()->id;
if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
(!empty($contenttype) && !in_array($contenttype, $this->formats))) {
throw new \Exception('No context or unsupported content type', 400);
}
if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
}
if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
throw new \Exception('Not allowed in context', 403);
}
if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
}
$item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
if ($item === false) {
throw new \Exception('Line item does not exist', 404);
}
$gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
$ltilinkid = null;
if (isset($item->iteminstance)) {
$ltilinkid = $item->iteminstance;
} else if ($gbs && isset($gbs->ltilinkid)) {
$ltilinkid = $gbs->ltilinkid;
}
if ($ltilinkid != null) {
if (is_null($typeid)) {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
$this->get_service()->get_tool_proxy()->id))) {
$response->set_code(403);
$response->set_reason("Invalid LTI id supplied.");
return;
}
} else {
if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
$typeid))) {
$response->set_code(403);
$response->set_reason("Invalid LTI id supplied.");
return;
}
}
}
$json = '[]';
require_once($CFG->libdir.'/gradelib.php');
switch ($response->get_request_method()) {
case 'GET':
$response->set_code(405);
$response->set_reason("GET requests are not allowed.");
break;
case 'POST':
try {
$json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid,
$typeid);
$response->set_content_type($this->formats[1]);
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
break;
default: // Should not be possible.
$response->set_code(405);
$response->set_reason("Invalid request method specified.");
return;
}
$response->set_body($json);
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
}
/**
* 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->scoreGiven) && !isset($score->scoreMaximum)) ||
(isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) ||
(!gradebookservices::is_user_gradable_in_course($contextid, $score->userId))
) {
throw new \Exception('Incorrect score received' . $body, 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;
}
}
$this->get_service()->save_grade_item($item, $score, $score->userId);
}
/**
* 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;
}
}
@@ -0,0 +1,901 @@
<?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;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
/**
* 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 {
/** Read-only access to Gradebook services */
const GRADEBOOKSERVICES_READ = 1;
/** Full access to Gradebook services */
const GRADEBOOKSERVICES_FULL = 2;
/** Scope for full access to Lineitem service */
const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
/** Scope for full access to Lineitem service */
const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
/** Scope for access to Result service */
const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
/** Scope for access to Score service */
const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'gradebookservices';
$this->name = get_string($this->get_component_id(), $this->get_component_id());
}
/**
* 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;
}
/**
* Get the scope(s) permitted for this service.
*
* @return array
*/
public function get_permitted_scopes() {
$scopes = array();
$ok = !empty($this->get_type());
if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) {
$scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
$scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
$scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE;
if ($setting == self::GRADEBOOKSERVICES_FULL) {
$scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM;
}
}
}
return $scopes;
}
/**
* Get the scopes defined by this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ,
self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM];
}
/**
* 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 = [
get_string('nevergs', $this->get_component_id()),
get_string('partialgs', $this->get_component_id()),
get_string('alwaysgs', $this->get_component_id())
];
$mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options);
$mform->setType($selectelementname, 'int');
$mform->setDefault($selectelementname, 0);
$mform->addHelpButton($selectelementname, $identifier, $this->get_component_id());
}
/**
* For submission review, if there is a dedicated URL, use it as the target link.
*
* @param string $messagetype message type for this launch
* @param string $targetlinkuri current target link uri
* @param string|null $customstr concatenated list of custom parameters
* @param int $courseid
* @param null|object $lti LTI Instance.
*
* @return array containing the target link URL and the custom params string to use.
*/
public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid,
?object $lti = null): array {
global $DB;
if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) {
$conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id);
$coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions);
if (count($coupledlineitems) == 1) {
$item = reset($coupledlineitems);
$url = $item->subreviewurl;
$subreviewparams = $item->subreviewparams;
if (!empty($url) && $url != 'DEFAULT') {
$targetlinkuri = $url;
}
if (!empty($subreviewparams)) {
if (!empty($customstr)) {
$customstr .= "\n{$subreviewparams}";
} else {
$customstr = $subreviewparams;
}
}
}
}
return [$targetlinkuri, $customstr];
}
/**
* Return an array of key/claim mapping allowing LTI 1.1 custom parameters
* to be transformed to LTI 1.3 claims.
*
* @return array Key/value pairs of params to claim mapping.
*/
public function get_jwt_claim_mappings(): array {
return [
'custom_gradebookservices_scope' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'scope',
'isarray' => true
],
'custom_lineitems_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'lineitems',
'isarray' => false
],
'custom_lineitem_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'lineitem',
'isarray' => false
],
'custom_results_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'results',
'isarray' => false
],
'custom_result_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'result',
'isarray' => false
],
'custom_scores_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'scores',
'isarray' => false
],
'custom_score_url' => [
'suffix' => 'ags',
'group' => 'endpoint',
'claim' => 'score',
'isarray' => false
]
];
}
/**
* 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();
$this->set_type(lti_get_type($typeid));
$this->set_typeconfig(lti_get_type_config($typeid));
// Only inject parameters if the service is enabled for this tool.
if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ ||
$this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) {
// 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)) {
$id = null;
if (!is_null($modlti)) {
$conditions = array('courseid' => $courseid, 'itemtype' => 'mod',
'itemmodule' => 'lti', 'iteminstance' => $modlti);
$coupledlineitems = $DB->get_records('grade_items', $conditions);
$conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti);
$lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs);
// If a link has more that one attached grade items, per spec we do not populate line item url.
if (count($lineitemsgbs) == 1) {
$id = reset($lineitemsgbs)->gradeitemid;
}
if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) {
$coupledid = reset($coupledlineitems)->id;
if (!is_null($id) && $id != $coupledid) {
$id = null;
} else {
$id = $coupledid;
}
}
}
$launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes());
$launchparameters['lineitems_url'] = '$LineItems.url';
if (!is_null($id)) {
$launchparameters['lineitem_url'] = '$LineItem.url';
}
}
}
}
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);
$sql = "SELECT i.*
FROM {grade_items} i
WHERE (i.courseid = :courseid)
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))
&& (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) {
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($resourceid) && !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;
}
/**
* Adds a decoupled (standalone) line item.
* Decoupled line items are not directly attached to
* an lti instance activity. They are recorded in
* the gradebook as manual activities and the
* gradebookservices is used to associate that manual column
* with the tool in addition to storing the LTI related
* metadata (resource id, tag).
*
* @param string $courseid ID of course
* @param string $label label of lineitem
* @param float $maximumscore maximum score of lineitem
* @param string $baseurl
* @param int|null $ltilinkid id of lti instance this line item is associated with
* @param string|null $resourceid resource id of lineitem
* @param string|null $tag tag of lineitem
* @param int $typeid lti type to which this line item is associated with
* @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to
*
* @return int id of the created gradeitem
*/
public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore,
string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid,
int $toolproxyid = null): int {
global $DB;
$params = array();
$params['itemname'] = $label;
$params['gradetype'] = GRADE_TYPE_VALUE;
$params['grademax'] = $maximumscore;
$params['grademin'] = 0;
$item = new \grade_item(array('id' => 0, 'courseid' => $courseid));
\grade_item::set_properties($item, $params);
$item->itemtype = 'manual';
$item->grademax = $maximumscore;
$id = $item->insert('mod/ltiservice_gradebookservices');
$DB->insert_record('ltiservice_gradebookservices', (object)array(
'gradeitemid' => $id,
'courseid' => $courseid,
'toolproxyid' => $toolproxyid,
'typeid' => $typeid,
'baseurl' => $baseurl,
'ltilinkid' => $ltilinkid,
'resourceid' => $resourceid,
'tag' => $tag
));
return $id;
}
/**
* Set a grade item.
*
* @param object $gradeitem Grade Item record
* @param object $score Result object
* @param int $userid User ID
*
* @throws \Exception
* @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
* @see gradebookservices::save_grade_item($gradeitem, $score, $userid)
*/
public static function save_score($gradeitem, $score, $userid) {
$service = new gradebookservices();
$service->save_grade_item($gradeitem, $score, $userid);
}
/**
* Saves a score received from the LTI tool.
*
* @param object $gradeitem Grade Item record
* @param object $score Result object
* @param int $userid User ID
*
* @throws \Exception
*/
public function save_grade_item($gradeitem, $score, $userid) {
global $DB, $CFG;
$source = 'mod' . $this->get_component_id();
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)) {
$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 (!empty($score->comment)) {
$feedback = $score->comment;
$feedbackformat = FORMAT_PLAIN;
}
if ($gradeitem->is_manual_item()) {
$result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified);
} else {
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;
$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);
$gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id);
if ($gbs) {
$lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : '';
$lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : '';
if (isset($gbs->ltilinkid)) {
$lineitem->resourceLinkId = strval($gbs->ltilinkid);
$lineitem->ltiLinkId = strval($gbs->ltilinkid);
}
if (!empty($gbs->subreviewurl)) {
$submissionreview = new \stdClass();
if ($gbs->subreviewurl != 'DEFAULT') {
$submissionreview->url = $gbs->subreviewurl;
}
if (!empty($gbs->subreviewparams)) {
$submissionreview->custom = lti_split_parameters($gbs->subreviewparams);
}
$lineitem->submissionReview = $submissionreview;
}
} else {
$lineitem->tag = '';
if (isset($item->iteminstance)) {
$lineitem->resourceLinkId = strval($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;
}
}
/**
* Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance.
*
* @param object $ltiinstance The lti instance to which the grade item is coupled to
* @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null.
* @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null.
* @param moodle_url|null $subreviewurl The submission review target link URL
* @param string|null $subreviewparams The submission review custom parameters.
*
*/
public static function update_coupled_gradebookservices(object $ltiinstance,
?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams): void {
global $DB;
if ($ltiinstance && $ltiinstance->typeid) {
$gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id));
if ($gradeitem) {
$resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid;
$subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null;
$tag = (isset($tag) && empty(trim($tag))) ? null : $tag;
$gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
if ($gbs) {
$gbs->resourceid = $resourceid;
$gbs->tag = $tag;
$gbs->subreviewurl = $subreviewurlstr;
$gbs->subreviewparams = $subreviewparams;
$DB->update_record('ltiservice_gradebookservices', $gbs);
} else {
$baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl;
$DB->insert_record('ltiservice_gradebookservices', (object)array(
'gradeitemid' => $gradeitem->id,
'courseid' => $gradeitem->courseid,
'typeid' => $ltiinstance->typeid,
'baseurl' => $baseurl,
'ltilinkid' => $ltiinstance->id,
'resourceid' => $resourceid,
'tag' => $tag,
'subreviewurl' => $subreviewurlstr,
'subreviewparams' => $subreviewparams
));
}
}
}
}
/**
* Called when a new LTI Instance is added.
*
* @param object $lti LTI Instance.
*/
public function instance_added(object $lti): void {
self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
$lti->lineitemsubreviewparams ?? null);
}
/**
* Called when a new LTI Instance is updated.
*
* @param object $lti LTI Instance.
*/
public function instance_updated(object $lti): void {
self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
$lti->lineitemsubreviewparams ?? null);
}
/**
* Set the form data when displaying the LTI Instance form.
*
* @param object $defaultvalues Default form values.
*/
public function set_instance_form_values(object $defaultvalues): void {
$defaultvalues->lineitemresourceid = '';
$defaultvalues->lineitemtag = '';
$defaultvalues->subreviewurl = '';
$defaultvalues->subreviewparams = '';
if (is_object($defaultvalues) && $defaultvalues->instance) {
$gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance);
if ($gbs) {
$defaultvalues->lineitemresourceid = $gbs->resourceid;
$defaultvalues->lineitemtag = $gbs->tag;
$defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl;
$defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams;
}
}
}
/**
* 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)";
$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 an lti instance
*
* @param string $instanceid The LTI module instance id
* @return object gradebookservice for this line item
*/
public static function find_ltiservice_gradebookservice_for_lti($instanceid) {
global $DB;
if ($instanceid) {
$gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid));
if ($gradeitem) {
return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
}
}
}
/**
* Find the right element in the ltiservice_gradebookservice table for a lineitem
*
* @param string $lineitemid The lineitem (gradeitem) id
* @return object gradebookservice if it exists
*/
public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) {
global $DB;
if ($lineitemid) {
return $DB->get_record('ltiservice_gradebookservices',
array('gradeitemid' => $lineitemid));
}
}
/**
* 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;
}
}
}
@@ -0,0 +1,113 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_gradebookservices.
*
* @package ltiservice_gradebookservices
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_gradebookservices\privacy;
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_gradebookservices.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->link_external_location('External LTI provider.', [
'userid' => 'privacy:metadata:userid',
'grade' => 'privacy:metadata:grade',
'maxgrade' => 'privacy:metadata:maxgrade',
'feedback' => 'privacy:metadata:feedback',
'timemodified' => 'privacy:metadata:timemodified'
], 'privacy:metadata:externalpurpose');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all user data which matches the specified context.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
}
@@ -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();
}
}
@@ -0,0 +1,28 @@
<?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="resourceid" TYPE="char" LENGTH="512" NOTNULL="false" SEQUENCE="false" COMMENT="Resource id for the line item"/>
<FIELD NAME="tag" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Tag type specified for the line item"/>
<FIELD NAME="subreviewurl" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Submission review URL"/>
<FIELD NAME="subreviewparams" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Submission review custom params"/>
</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>
@@ -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' => '*'
)
);
@@ -0,0 +1,69 @@
<?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 is part of BasicLTI4Moodle
//
// BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
// consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
// based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
// specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
// are already supporting or going to support BasicLTI. This project Implements the consumer
// for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
// BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
// at the GESSI research group at UPC.
// SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
// by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
// Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
//
// BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
// of the Universitat Politecnica de Catalunya http://www.upc.edu
// Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
/**
* This file defines tasks performed by the plugin.
*
* @package ltiservice_gradebookservices
* @copyright 2020 Cengage Learning http://www.cengage.com
* @author Claude Vervoort
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* xmldb_ltiservice_gradebookservices_upgrade is the function that upgrades
* the gradebook lti service subplugin database when is needed.
*
* This function is automatically called when version number in
* version.php changes.
*
* @param int $oldversion New old version number.
*
* @return boolean
*/
function xmldb_ltiservice_gradebookservices_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,44 @@
<?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'] = 'Whether to use the IMS LTI Assignment and Grade Services to synchronise grades instead of the Basic Outcomes service.
* **Do not use this service** - Basic Outcomes features and configuration will be used
* **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['ltiservice_gradebookservices'] = 'IMS LTI Assignment and Grade Services';
$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['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
$string['privacy:metadata:feedback'] = 'The feedback the user received for this LTI activity.';
$string['privacy:metadata:grade'] = 'The grade the user received in Moodle for this LTI activity.';
$string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.';
$string['privacy:metadata:timemodified'] = 'The last time the grade was updated';
$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
$string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup';
@@ -0,0 +1,409 @@
<?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/>.
namespace ltiservice_gradebookservices;
use ltiservice_gradebookservices\local\service\gradebookservices;
/**
* Unit tests for lti gradebookservices.
*
* @package ltiservice_gradebookservices
* @category test
* @copyright 2020 Claude Vervoort <claude.vervoort@cengage.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_lti\service\gradebookservices\local\gradebookservices
*/
class gradebookservices_test extends \advanced_testcase {
/**
* Load the necessary libs for the tests.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
}
/**
* @covers ::instance_added
*
* Test saving a graded LTI with resource and tag info (as a result of
* content item selection) creates a gradebookservices record
* that can be retrieved using the gradebook service API.
*/
public function test_lti_add_coupled_lineitem(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$resourceid = 'test-resource-id';
$tag = 'tag';
$subreviewurl = 'https://subreview.example.com';
$subreviewparams = 'a=2';
$ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams);
$this->assertNotNull($ltiinstance);
$gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id);
$this->assertNotNull($gbs);
$this->assertEquals($resourceid, $gbs->resourceid);
$this->assertEquals($tag, $gbs->tag);
$this->assertEquals($subreviewurl, $gbs->subreviewurl);
$this->assertEquals($subreviewparams, $gbs->subreviewparams);
$this->assert_lineitems($course, $typeid, $ltiinstance->name,
$ltiinstance, $resourceid, $tag, $subreviewurl, $subreviewparams);
}
/**
* @covers ::instance_added
*
* Test saving a graded LTI with resource and tag info (as a result of
* content item selection) creates a gradebookservices record
* that can be retrieved using the gradebook service API.
*/
public function test_lti_add_coupled_lineitem_default_subreview(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$resourceid = 'test-resource-id';
$tag = 'tag';
$ltiinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag, 'DEFAULT');
$this->assertNotNull($ltiinstance);
$gbs = gradebookservices::find_ltiservice_gradebookservice_for_lti($ltiinstance->id);
$this->assertNotNull($gbs);
$this->assertEquals('DEFAULT', $gbs->subreviewurl);
$this->assert_lineitems($course, $typeid, $ltiinstance->name, $ltiinstance, $resourceid, $tag, 'DEFAULT');
}
/**
* @covers ::add_standalone_lineitem
*
* Test saving a standalone LTI lineitem with resource and tag info
* that can be retrieved using the gradebook service API.
*/
public function test_lti_add_standalone_lineitem(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$resourceid = "test-resource-standalone";
$tag = "test-tag-standalone";
$typeid = $this->create_type();
$this->create_standalone_lineitem($course->id, $typeid, $resourceid, $tag);
$this->assert_lineitems($course, $typeid, "manualtest", null, $resourceid, $tag);
}
/**
* @covers ::find_ltiservice_gradebookservice_for_lti
*
* Test line item URL is populated for coupled line item only
* if there is not another line item bound to the lti instance,
* since in that case there would be no rule to define which of
* the line items should be actually passed.
*/
public function test_get_launch_parameters_coupled(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag', 'https://subreview.url', 'sub=review');
$this->assertNotNull($ltiinstance);
$gbservice = new gradebookservices();
$params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id);
$this->assertEquals('$LineItem.url', $params['lineitem_url']);
$this->assertEquals('$LineItem.url', $params['lineitem_url']);
$this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id);
$params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id);
$this->assertEquals('$LineItems.url', $params['lineitems_url']);
// 2 line items for a single link, we cannot return a single line item url.
$this->assertFalse(array_key_exists('$LineItem.url', $params));
}
/**
* @covers ::override_endpoint
*
* Test Submission Review URL and custom parameter is applied when the
* launch is submission review.
*/
public function test_get_launch_parameters_coupled_subreview_override(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag',
'https://example.com/subreview', 'action=review');
$this->assertNotNull($ltiinstance);
$gbservice = new gradebookservices();
$overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti',
"color=blue", $course->id, $ltiinstance);
$this->assertEquals('https://example.com/subreview', $overrides[0]);
$this->assertEquals("color=blue\naction=review", $overrides[1]);
}
/**
* @covers ::override_endpoint
*
* Test Submission Review URL and custom parameter is applied when the
* launch is submission review.
*/
public function test_get_launch_parameters_coupled_subreview_override_default(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$ltiinstance = $this->create_graded_lti($typeid, $course, 'resource-id', 'tag',
'DEFAULT', '');
$this->assertNotNull($ltiinstance);
$gbservice = new gradebookservices();
$overrides = $gbservice->override_endpoint('LtiSubmissionReviewRequest', 'https://example.com/lti',
"color=blue", $course->id, $ltiinstance);
$this->assertEquals('https://example.com/lti', $overrides[0]);
$this->assertEquals("color=blue", $overrides[1]);
}
/**
* @covers ::get_launch_parameters
*
* Test line item URL is populated for not coupled line item only
* if there is a single line item attached to that lti instance.
*/
public function test_get_launch_parameters_decoupled(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a tool type, associated with that proxy.
$typeid = $this->create_type();
$course = $this->getDataGenerator()->create_course();
$ltiinstance = $this->create_notgraded_lti($typeid, $course);
$this->assertNotNull($ltiinstance);
$gbservice = new gradebookservices();
$params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id);
$this->assertEquals('$LineItems.url', $params['lineitems_url']);
$this->assertFalse(array_key_exists('$LineItem.url', $params));
$this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag', $ltiinstance->id);
$params = $gbservice->get_launch_parameters('basic-lti-launch-request', $course->id, 111, $typeid, $ltiinstance->id);
$this->assertEquals('$LineItems.url', $params['lineitems_url']);
$this->assertEquals('$LineItem.url', $params['lineitem_url']);
// 2 line items for a single link, we cannot return a single line item url.
$this->create_standalone_lineitem($course->id, $typeid, 'resource-id', 'tag-2', $ltiinstance->id);
$this->assertFalse(array_key_exists('$LineItem.url', $params));
}
/**
* @covers ::is_user_gradable_in_course
*
* Test if a user can be graded in a course.
*/
public function test_is_user_gradable_in_course(): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$generator->enrol_user($user1->id, $course->id, 'student');
$generator->enrol_user($user2->id, $course->id, 'editingteacher');
$this->assertTrue(gradebookservices::is_user_gradable_in_course($course->id, $user1->id));
$this->assertFalse(gradebookservices::is_user_gradable_in_course($course->id, $user2->id));
}
/**
* Asserts a matching gradebookservices record exist with the matching tag and resourceid.
*
* @param object $course current course
* @param int $typeid Type id of the tool
* @param string $label Label of the line item
* @param object|null $ltiinstance lti instance related to that line item
* @param string|null $resourceid resourceid the line item should have
* @param string|null $tag tag the line item should have
* @param string|null $subreviewurl submission review url
* @param string|null $subreviewparams submission review custom params
*/
private function assert_lineitems(object $course, int $typeid,
string $label, ?object $ltiinstance, ?string $resourceid, ?string $tag,
?string $subreviewurl = null, ?string $subreviewparams = null): void {
$gbservice = new gradebookservices();
$gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid);
// The 1st item in the array is the items count.
$this->assertEquals(1, $gradeitems[0]);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertEquals(10, $lineitem->scoreMaximum);
$this->assertEquals($resourceid, $lineitem->resourceId);
$this->assertEquals($tag, $lineitem->tag);
$this->assertEquals($label, $lineitem->label);
$this->assertEquals(!empty($subreviewurl), isset($lineitem->submissionReview));
if ($subreviewurl) {
if ($subreviewurl == 'DEFAULT') {
$this->assertFalse(isset($lineitem->submissionReview->url));
} else {
$this->assertEquals($subreviewurl, $lineitem->submissionReview->url);
}
if ($subreviewparams) {
$custom = $lineitem->submissionReview->custom;
$this->assertEquals($subreviewparams, join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom))));
} else {
$this->assertFalse(isset($lineitem->submissionReview->custom));
}
}
$gradeitems = $gbservice->get_lineitems($course->id, $resourceid, null, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
if (isset($ltiinstance)) {
$gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
$gradeitems = $gbservice->get_lineitems($course->id, null, $ltiinstance->id + 1, null, null, null, $typeid);
$this->assertEquals(0, $gradeitems[0]);
}
$gradeitems = $gbservice->get_lineitems($course->id, null, null, $tag, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
$gradeitems = $gbservice->get_lineitems($course->id, 'an unknown resource id', null, null, null, null, $typeid);
$this->assertEquals(0, $gradeitems[0]);
$gradeitems = $gbservice->get_lineitems($course->id, null, null, 'an unknown tag', null, null, $typeid);
$this->assertEquals(0, $gradeitems[0]);
}
/**
* Inserts a graded lti instance, which should create a grade_item and gradebookservices record.
*
* @param int $typeid Type ID of the LTI Tool.
* @param object $course course where to add the lti instance.
* @param string|null $resourceid resource id
* @param string|null $tag tag
* @param string|null $subreviewurl submission review url
* @param string|null $subreviewparams submission review custom params
*
* @return object lti instance created
*/
private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag,
?string $subreviewurl = null, ?string $subreviewparams = null): object {
$lti = ['course' => $course->id,
'typeid' => $typeid,
'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS,
'grade' => 10,
'lineitemresourceid' => $resourceid,
'lineitemtag' => $tag,
'lineitemsubreviewurl' => $subreviewurl,
'lineitemsubreviewparams' => $subreviewparams];
return $this->getDataGenerator()->create_module('lti', $lti, array());
}
/**
* Inserts an lti instance that is not graded.
*
* @param int $typeid Type Id of the LTI Tool.
* @param object $course course where to add the lti instance.
*
* @return object lti instance created
*/
private function create_notgraded_lti(int $typeid, object $course): object {
$lti = ['course' => $course->id,
'typeid' => $typeid,
'instructorchoiceacceptgrades' => LTI_SETTING_NEVER];
return $this->getDataGenerator()->create_module('lti', $lti, array());
}
/**
* Inserts a standalone lineitem (gradeitem, gradebookservices entries).
*
* @param int $courseid Id of the course where the standalone line item will be added.
* @param int $typeid of the LTI Tool
* @param string|null $resourceid resource id
* @param string|null $tag tag
* @param int|null $ltiinstanceid Id of the LTI instance the standalone line item will be related to.
*
*/
private function create_standalone_lineitem(int $courseid, int $typeid, ?string $resourceid,
?string $tag, int $ltiinstanceid = null): void {
$gbservice = new gradebookservices();
$gbservice->add_standalone_lineitem($courseid,
"manualtest",
10,
"https://test.phpunit",
$ltiinstanceid,
$resourceid,
$tag,
$typeid,
null /*toolproxyid*/);
}
/**
* Creates a new LTI Tool Type.
*/
private function create_type() {
$type = new \stdClass();
$type->state = LTI_TOOL_STATE_CONFIGURED;
$type->name = "Test tool";
$type->description = "Example description";
$type->clientid = "Test client ID";
$type->baseurl = $this->getExternalTestFileUrl('/test.html');
$config = new \stdClass();
$config->ltiservice_gradesynchronization = 2;
return lti_add_type($type, $config);
}
}
@@ -0,0 +1,236 @@
<?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/>.
namespace ltiservice_gradebookservices;
use ltiservice_gradebookservices\local\resources\lineitem;
use ltiservice_gradebookservices\local\service\gradebookservices;
/**
* Unit tests for lti lineitem.
*
* @package ltiservice_gradebookservices
* @category test
* @copyright 2022 Cengage Group <claude.vervoort@cengage.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_lti\service\gradebookservices\local\resources\lineitem
*/
class lineitem_test extends \advanced_testcase {
/**
* @covers ::execute
*
* Test updating the line item with submission review.
*/
public function test_execute_put_nosubreview(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
$this->resetAfterTest();
$this->setAdminUser();
$resourceid = 'test-resource-id';
$tag = 'tag';
$course = $this->getDataGenerator()->create_course();
$typeid = $this->create_type();
// The 1st item in the array is the items count.
$gbservice = new gradebookservices();
$gbservice->set_type(lti_get_type($typeid));
$this->create_graded_lti($typeid, $course, $resourceid, $tag);
$gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertFalse(isset($lineitem->submissionReview));
$lineitemresource = new lineitem($gbservice);
$this->set_server_for_put($course, $typeid, $lineitem);
$response = new \mod_lti\local\ltiservice\response();
$lineitem->resourceId = $resourceid.'modified';
$lineitem->tag = $tag.'modified';
$response->set_request_data(json_encode($lineitem));
$lineitemresource->execute($response);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertFalse(isset($lineitem->submissionReview));
$this->assertEquals($resourceid.'modified', $lineitem->resourceId);
$this->assertEquals($tag.'modified', $lineitem->tag);
$responseitem = json_decode($response->get_body());
$this->assertEquals($resourceid.'modified', $responseitem->resourceId);
}
/**
* @covers ::execute
*
* Test updating the line item with submission review.
*/
public function test_execute_put_withsubreview(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
$this->resetAfterTest();
$this->setAdminUser();
$resourceid = 'test-resource-id';
$tag = 'tag';
$subreviewurl = 'https://subreview.example.com';
$subreviewparams = 'a=2';
$course = $this->getDataGenerator()->create_course();
$typeid = $this->create_type();
// The 1st item in the array is the items count.
$gbservice = new gradebookservices();
$gbservice->set_type(lti_get_type($typeid));
$this->create_graded_lti($typeid, $course, $resourceid, $tag, $subreviewurl, $subreviewparams);
$gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertTrue(isset($lineitem->submissionReview));
$lineitemresource = new lineitem($gbservice);
$this->set_server_for_put($course, $typeid, $lineitem);
$response = new \mod_lti\local\ltiservice\response();
$lineitem->resourceId = $resourceid.'modified';
$lineitem->tag = $tag.'modified';
$lineitem->submissionReview->url = $subreviewurl.'modified';
$lineitem->submissionReview->custom = ['a' => '3'];
$response->set_request_data(json_encode($lineitem));
$lineitemresource->execute($response);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertEquals($resourceid.'modified', $lineitem->resourceId);
$this->assertEquals($subreviewurl.'modified', $lineitem->submissionReview->url);
$custom = $lineitem->submissionReview->custom;
$this->assertEquals('a=3', join("\n", array_map(fn($k) => $k.'='.$custom[$k], array_keys($custom))));
$responseitem = json_decode($response->get_body());
$this->assertEquals($resourceid.'modified', $responseitem->resourceId);
$this->assertEquals($subreviewurl.'modified', $responseitem->submissionReview->url);
}
/**
* @covers ::execute
*
* Test updating the line item with submission review.
*/
public function test_execute_put_addsubreview(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
$this->resetAfterTest();
$this->setAdminUser();
$resourceid = 'test-resource-id';
$tag = 'tag';
$subreviewurl = 'https://subreview.example.com';
$course = $this->getDataGenerator()->create_course();
$typeid = $this->create_type();
// The 1st item in the array is the items count.
$gbservice = new gradebookservices();
$gbservice->set_type(lti_get_type($typeid));
$this->create_graded_lti($typeid, $course, $resourceid, $tag);
$gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertFalse(isset($lineitem->submissionReview));
$lineitemresource = new lineitem($gbservice);
$this->set_server_for_put($course, $typeid, $lineitem);
$response = new \mod_lti\local\ltiservice\response();
$lineitem->resourceId = $resourceid.'modified';
$lineitem->tag = $tag.'modified';
$lineitem->submissionReview = ['url' => $subreviewurl];
$response->set_request_data(json_encode($lineitem));
$lineitemresource->execute($response);
$lineitem = gradebookservices::item_for_json($gradeitems[1][0], '', $typeid);
$this->assertEquals($resourceid.'modified', $lineitem->resourceId);
$this->assertEquals($subreviewurl, $lineitem->submissionReview->url);
$this->assertFalse(isset($lineitem->submissionReview->custom));
$responseitem = json_decode($response->get_body());
$this->assertEquals($resourceid.'modified', $responseitem->resourceId);
$this->assertEquals($subreviewurl, $responseitem->submissionReview->url);
$this->assertFalse(isset($responseitem->submissionReview->custom));
}
/**
* Inserts a graded lti instance, which should create a grade_item and gradebookservices record.
*
* @param int $typeid Type ID of the LTI Tool.
* @param object $course course where to add the lti instance.
* @param string|null $resourceid resource id
* @param string|null $tag tag
* @param string|null $subreviewurl submission review url
* @param string|null $subreviewparams submission review custom params
*
* @return object lti instance created
*/
private function create_graded_lti(int $typeid, object $course, ?string $resourceid, ?string $tag,
?string $subreviewurl = null, ?string $subreviewparams = null): object {
$lti = ['course' => $course->id,
'typeid' => $typeid,
'instructorchoiceacceptgrades' => LTI_SETTING_ALWAYS,
'grade' => 10,
'lineitemresourceid' => $resourceid,
'lineitemtag' => $tag,
'lineitemsubreviewurl' => $subreviewurl,
'lineitemsubreviewparams' => $subreviewparams];
return $this->getDataGenerator()->create_module('lti', $lti, array());
}
/**
* Creates a new LTI Tool Type.
*/
private function create_type() {
$type = new \stdClass();
$type->state = LTI_TOOL_STATE_CONFIGURED;
$type->name = "Test tool";
$type->description = "Example description";
$type->clientid = "Test client ID";
$type->baseurl = $this->getExternalTestFileUrl('/test.html');
$config = new \stdClass();
$config->ltiservice_gradesynchronization = 2;
return lti_add_type($type, $config);
}
/**
* Sets the server info and get to be configured for a PUT operation,
* including having a proper auth token attached.
*
* @param object $course course where to add the lti instance.
* @param int $typeid
* @param object $lineitem
*/
private function set_server_for_put(object $course, int $typeid, object $lineitem) {
$_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_PUT;
$_SERVER['PATH_INFO'] = "/$course->id/lineitems$lineitem->id";
$token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem']);
$_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token;
$_GET['type_id'] = (string)$typeid;
}
}
@@ -0,0 +1,52 @@
<?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/>.
/**
* Unit tests for ltiservice_gradebookservices privacy provider.
*
* @package ltiservice_gradebookservices
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_gradebookservices\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\tests\provider_testcase;
/**
* Unit tests for ltiservice_gradebookservices privacy provider.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Basic setup for these tests.
*/
public function setUp(): void {
$this->resetAfterTest(true);
}
/**
* Test getting the context for the user ID related to this plugin.
*/
public function test_get_contexts_for_userid(): void {
$user = $this->getDataGenerator()->create_user();
$contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id);
$this->assertEmpty($contextlist);
}
}
@@ -0,0 +1,136 @@
<?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/>.
namespace ltiservice_gradebookservices\task;
/**
* Tests cleaning up the gradebook services task.
*
* @package ltiservice_gradebookservices
* @category test
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cleanup_test extends \advanced_testcase {
/**
* Test set up.
*
* This is executed before running any test in this file.
*/
public function setUp(): void {
$this->resetAfterTest();
}
/**
* Test the cleanup task.
*/
public function test_cleanup_task(): void {
global $DB;
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a few LTI items.
$lti = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]);
$lti2 = $this->getDataGenerator()->create_module('lti', ['course' => $course->id]);
$conditions = [
'courseid' => $course->id,
'itemtype' => 'mod',
'itemmodule' => 'lti',
'iteminstance' => $lti->id
];
// Get the grade items.
$gradeitem = $DB->get_record('grade_items', $conditions);
$conditions['iteminstance'] = $lti2->id;
$gradeitem2 = $DB->get_record('grade_items', $conditions);
// Insert these into the 'ltiservice_gradebookservices' table.
$data = new \stdClass();
$data->gradeitemid = $gradeitem->id;
$data->courseid = $course->id;
$DB->insert_record('ltiservice_gradebookservices', $data);
$data->gradeitemid = $gradeitem2->id;
$DB->insert_record('ltiservice_gradebookservices', $data);
$task = new cleanup_task();
$task->execute();
// Check they both still exist.
$this->assertEquals(2, $DB->count_records('ltiservice_gradebookservices'));
// Delete the first LTI activity.
course_delete_module($lti->cmid);
// Run the task again.
$task = new cleanup_task();
$task->execute();
// Check only the second grade item exists.
$gradebookserviceitems = $DB->get_records('ltiservice_gradebookservices');
$this->assertCount(1, $gradebookserviceitems);
$gradebookserviceitem = reset($gradebookserviceitems);
$this->assertEquals($gradeitem2->id, $gradebookserviceitem->gradeitemid);
}
/**
* Test the cleanup task with a manual grade item.
*/
public function test_cleanup_task_with_manual_item(): void {
global $CFG, $DB;
// This is required when running the unit test in isolation.
require_once($CFG->libdir . '/gradelib.php');
// Create a manual grade item for a course.
$course = $this->getDataGenerator()->create_course();
$params = [
'courseid' => $course->id,
'itemtype' => 'manual'
];
$gradeitem = new \grade_item($params);
$gradeitem->insert();
// Insert it into the 'ltiservice_gradebookservices' table.
$data = new \stdClass();
$data->gradeitemid = $gradeitem->id;
$data->courseid = $course->id;
$DB->insert_record('ltiservice_gradebookservices', $data);
// Run the task.
$task = new cleanup_task();
$task->execute();
// Check it still exist.
$this->assertEquals(1, $DB->count_records('ltiservice_gradebookservices'));
// Delete the manual item.
$gradeitem->delete();
// Run the task again.
$task = new cleanup_task();
$task->execute();
// Check it has been removed.
$this->assertEquals(0, $DB->count_records('ltiservice_gradebookservices'));
}
}
@@ -0,0 +1,6 @@
This files describes API changes in the lti code.
=== 4.1 ===
* The update_coupled_gradebookservices function now accept 2 additional optional parameters
to update the newly added properties related to submission review support (url and custom params).
@@ -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 = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_gradebookservices';
@@ -0,0 +1,147 @@
<?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 Context Memberships resource
*
* @package ltiservice_memberships
* @copyright 2015 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_memberships\local\resources;
use mod_lti\local\ltiservice\resource_base;
use ltiservice_memberships\local\service\memberships;
use core_availability\info_module;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing Context Memberships.
*
* @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 contextmemberships extends resource_base {
/**
* Class constructor.
*
* @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/{tool_code}/memberships';
$this->variables[] = 'ToolProxyBinding.memberships.url';
$this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+json';
$this->formats[] = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json';
$this->methods[] = self::HTTP_GET;
}
/**
* 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();
$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->check_tool($params['tool_code'], $response->get_request_data(),
array(memberships::SCOPE_MEMBERSHIPS_READ))) {
throw new \Exception(null, 401);
}
if (!($course = $DB->get_record('course', array('id' => $params['context_id']), 'id,shortname,fullname',
IGNORE_MISSING))) {
throw new \Exception("Not Found: Course {$params['context_id']} doesn't exist", 404);
}
if (!$this->get_service()->is_allowed_in_context($params['tool_code'], $course->id)) {
throw new \Exception(null, 404);
}
if (!($context = \context_course::instance($course->id))) {
throw new \Exception("Not Found: Course instance {$course->id} doesn't exist", 404);
}
if (!empty($linkid)) {
if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) {
throw new \Exception("Not Found: LTI link {$linkid} doesn't exist", 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 = $this->get_service()->get_members_json($this, $context, $course, $role, $limitfrom, $limitnum, $lti,
$modinfo, $response);
$response->set_body($json);
} catch (\Exception $e) {
$response->set_code($e->getCode());
$response->set_reason($e->getMessage());
}
}
/**
* Parse a value for custom parameter substitution variables.
*
* @param string $value String to be parsed
*
* @return string
*/
public function parse_value($value) {
global $COURSE, $DB;
if (strpos($value, '$ToolProxyBinding.memberships.url') !== false) {
if ($COURSE->id === SITEID) {
$this->params['context_type'] = 'Group';
} else {
$this->params['context_type'] = 'CourseSection';
}
$this->params['context_id'] = $COURSE->id;
if ($tool = $this->get_service()->get_type()) {
$this->params['tool_code'] = $tool->id;
}
$value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value);
}
return $value;
}
}
@@ -0,0 +1,153 @@
<?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 Link Memberships resource
*
* @package ltiservice_memberships
* @copyright 2015 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_memberships\local\resources;
use mod_lti\local\ltiservice\resource_base;
use ltiservice_memberships\local\service\memberships;
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 resource_base {
/**
* Class constructor.
*
* @param \ltiservice_memberships\local\service\memberships $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'LtiLinkMemberships';
$this->template = '/links/{link_id}/memberships';
$this->variables[] = 'LtiLink.memberships.url';
$this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+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 $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;
}
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;
}
if (!$this->check_tool($lti->typeid, $response->get_request_data(), array(memberships::SCOPE_MEMBERSHIPS_READ))) {
$response->set_code(403);
return;
}
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 = $this->get_service()->get_members_json($this, $context, $course, $role,
$limitfrom, $limitnum, $lti, $info, $response);
$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();
}
}
/**
* Parse a value for custom parameter substitution variables.
*
* @param string $value String to be parsed
*
* @return string
*/
public function parse_value($value) {
if (strpos($value, '$LtiLink.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);
}
return $value;
}
}
@@ -0,0 +1,579 @@
<?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 Memberships service
*
* @package ltiservice_memberships
* @copyright 2015 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_memberships\local\service;
defined('MOODLE_INTERNAL') || die();
/**
* A service implementing Memberships.
*
* @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 memberships extends \mod_lti\local\ltiservice\service_base {
/** Default prefix for context-level roles */
const CONTEXT_ROLE_PREFIX = 'http://purl.imsglobal.org/vocab/lis/v2/membership#';
/** Context-level role for Instructor */
const CONTEXT_ROLE_INSTRUCTOR = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor';
/** Context-level role for Learner */
const CONTEXT_ROLE_LEARNER = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner';
/** Capability used to identify Instructors */
const INSTRUCTOR_CAPABILITY = 'moodle/course:manageactivities';
/** 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);
/** Scope for reading membership data */
const SCOPE_MEMBERSHIPS_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'memberships';
$this->name = get_string($this->get_component_id(), $this->get_component_id());
}
/**
* Get the resources for this service.
*
* @return array
*/
public function get_resources() {
if (empty($this->resources)) {
$this->resources = array();
$this->resources[] = new \ltiservice_memberships\local\resources\contextmemberships($this);
$this->resources[] = new \ltiservice_memberships\local\resources\linkmemberships($this);
}
return $this->resources;
}
/**
* Get the scope(s) permitted for the tool relevant to this service.
*
* @return array
*/
public function get_permitted_scopes() {
$scopes = array();
$ok = !empty($this->get_type());
if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) &&
($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) {
$scopes[] = self::SCOPE_MEMBERSHIPS_READ;
}
return $scopes;
}
/**
* Get the scope(s) defined by this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_MEMBERSHIPS_READ];
}
/**
* Get the JSON for members.
*
* @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request
* @param \context_course $context Course context
* @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 \core_availability\info_module $info Conditional availability information
* for LTI instance (null if context-level request)
*
* @return string
* @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
* @see memberships::get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response)
*/
public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) {
global $DB;
debugging('get_users_json() has been deprecated, ' .
'please use memberships::get_members_json() instead.', DEBUG_DEVELOPER);
$course = $DB->get_record('course', array('id' => $contextid), 'id,shortname,fullname', IGNORE_MISSING);
$memberships = new memberships();
$memberships->check_tool($tool->id, null, array(self::SCOPE_MEMBERSHIPS_READ));
$response = new \mod_lti\local\ltiservice\response();
$json = $memberships->get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response);
return $json;
}
/**
* Get the JSON for members.
*
* @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request
* @param \context_course $context Course context
* @param \course $course Course
* @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 \core_availability\info_module $info Conditional availability information
* for LTI instance (null if context-level request)
* @param \mod_lti\local\ltiservice\response $response Response object for the request
*
* @return string
*/
public function get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) {
$withcapability = '';
$exclude = array();
if (!empty($role)) {
if ((strpos($role, 'http://') !== 0) && (strpos($role, 'https://') !== 0)) {
$role = self::CONTEXT_ROLE_PREFIX . $role;
}
if ($role === self::CONTEXT_ROLE_INSTRUCTOR) {
$withcapability = self::INSTRUCTOR_CAPABILITY;
} else if ($role === self::CONTEXT_ROLE_LEARNER) {
$exclude = array_keys(get_enrolled_users($context, self::INSTRUCTOR_CAPABILITY, 0, 'u.id',
null, null, null, true));
}
}
$users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, 0, 0, true);
if (($response->get_accept() === 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json') ||
(($response->get_accept() !== 'application/vnd.ims.lis.v2.membershipcontainer+json') &&
($this->get_type()->ltiversion === LTI_VERSION_1P3))) {
$json = $this->users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response);
} else {
$json = $this->users_to_jsonld($resource, $users, $course->id, $exclude, $limitfrom, $limitnum, $lti, $info, $response);
}
return $json;
}
/**
* Get the JSON-LD representation of the users.
*
* Note that when a limit is set and the exclude array is not empty, then the number of memberships
* returned may be less than the limit.
*
* @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request
* @param array $users Array of user records
* @param string $contextid Course ID
* @param array $exclude Array of user records to be excluded from the response
* @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 \core_availability\info_module $info Conditional availability information
* for LTI instance (null if context-level request)
* @param \mod_lti\local\ltiservice\response $response Response object for the request
*
* @return string
*/
private function users_to_jsonld($resource, $users, $contextid, $exclude, $limitfrom, $limitnum,
$lti, $info, $response) {
global $DB;
$tool = $this->get_type();
$toolconfig = $this->get_typeconfig();
$arrusers = [
'@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer',
'@type' => 'Page',
'@id' => $resource->get_endpoint(),
];
$arrusers['pageOf'] = [
'@type' => 'LISMembershipContainer',
'membershipSubject' => [
'@type' => 'Context',
'contextId' => $contextid,
'membership' => []
]
];
$enabledcapabilities = lti_get_enabled_capabilities($tool);
$islti2 = $tool->toolproxyid > 0;
$n = 0;
$more = false;
foreach ($users as $user) {
if (in_array($user->id, $exclude)) {
continue;
}
if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) {
continue;
}
$n++;
if ($limitnum > 0) {
if ($n <= $limitfrom) {
continue;
}
if (count($arrusers['pageOf']['membershipSubject']['membership']) >= $limitnum) {
$more = true;
break;
}
}
$member = new \stdClass();
$member->{"@type" } = 'LISPerson';
$membership = new \stdClass();
$membership->status = 'Active';
$membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true));
$instanceconfig = null;
if (!is_null($lti)) {
$instanceconfig = lti_get_type_config_from_instance($lti->id);
}
$isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig,
['name' => 'sendname', 'email' => '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)],
'User.username' => ['type' => 'name',
'member.field' => 'ext_user_username',
'source.value' => format_string($user->username)]
];
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));
// Not per specification but added to comply with earlier version of the service.
$member->resultSourcedId = $message->lis_result_sourcedid;
}
$membership->message = [$message];
}
foreach ($includedcapabilities as $capabilityname => $capability) {
if ($islti2) {
if (in_array($capabilityname, $enabledcapabilities)) {
$member->{$capability['member.field']} = $capability['source.value'];
}
} 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;
$arrusers['pageOf']['membershipSubject']['membership'][] = $membership;
}
if ($more) {
$nextlimitfrom = $limitfrom + $limitnum;
$nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}";
if (!is_null($lti)) {
$nextpage .= "&rlid={$lti->id}";
}
$arrusers['nextPage'] = $nextpage;
}
$response->set_content_type('application/vnd.ims.lis.v2.membershipcontainer+json');
return json_encode($arrusers);
}
/**
* Get the NRP service JSON representation of the users.
*
* Note that when a limit is set and the exclude array is not empty, then the number of memberships
* returned may be less than the limit.
*
* @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request
* @param array $users Array of user records
* @param \course $course Course
* @param array $exclude Array of user records to be excluded from the response
* @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 \core_availability\info_module $info Conditional availability information for LTI instance
* @param \mod_lti\local\ltiservice\response $response Response object for the request
*
* @return string
*/
private function users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum,
$lti, $info, $response) {
global $DB, $CFG;
$tool = $this->get_type();
$toolconfig = $this->get_typeconfig();
$context = new \stdClass();
$context->id = $course->id;
$context->label = trim(html_to_text($course->shortname, 0));
$context->title = trim(html_to_text($course->fullname, 0));
$arrusers = [
'id' => $resource->get_endpoint(),
'context' => $context,
'members' => []
];
$islti2 = $tool->toolproxyid > 0;
$n = 0;
$more = false;
foreach ($users as $user) {
if (in_array($user->id, $exclude)) {
continue;
}
if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) {
continue;
}
$n++;
if ($limitnum > 0) {
if ($n <= $limitfrom) {
continue;
}
if (count($arrusers['members']) >= $limitnum) {
$more = true;
break;
}
}
$member = new \stdClass();
$member->status = 'Active';
$member->roles = explode(',', lti_get_ims_role($user->id, null, $course->id, true));
$instanceconfig = null;
if (!is_null($lti)) {
$instanceconfig = lti_get_type_config_from_instance($lti->id);
}
if (!$islti2) {
$isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig,
['name' => 'sendname', 'givenname' => 'sendname', 'familyname' => 'sendname',
'email' => 'sendemailaddr']);
} else {
$isallowedlticonfig = self::is_allowed_capability_set($tool,
['name' => 'Person.name.full', 'givenname' => 'Person.name.given',
'familyname' => 'Person.name.family', 'email' => 'Person.email.primary']);
}
$includedcapabilities = [
'User.id' => ['type' => 'id',
'member.field' => 'user_id',
'source.value' => $user->id],
'Person.sourcedId' => ['type' => 'id',
'member.field' => 'lis_person_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' => 'givenname',
'member.field' => 'given_name',
'source.value' => format_string($user->firstname)],
'Person.name.family' => ['type' => 'familyname',
'member.field' => 'family_name',
'source.value' => format_string($user->lastname)],
'Person.email.primary' => ['type' => 'email',
'member.field' => 'email',
'source.value' => format_string($user->email)],
'User.username' => ['type' => 'name',
'member.field' => 'ext_user_username',
'source.value' => format_string($user->username)],
];
if (!is_null($lti)) {
$message = new \stdClass();
$message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} = 'LtiResourceLinkRequest';
$conditions = array('courseid' => $course->id, 'itemtype' => 'mod',
'itemmodule' => 'lti', 'iteminstance' => $lti->id);
if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) {
$basicoutcome = new \stdClass();
$basicoutcome->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id,
$user->id,
$lti->servicesalt,
$lti->typeid));
// Add outcome service URL.
$serviceurl = new \moodle_url('/mod/lti/service.php');
$serviceurl = $serviceurl->out();
$forcessl = false;
if (!empty($CFG->mod_lti_forcessl)) {
$forcessl = true;
}
if ((isset($toolconfig['forcessl']) && ($toolconfig['forcessl'] == '1')) or $forcessl) {
$serviceurl = lti_ensure_url_is_https($serviceurl);
}
$basicoutcome->lis_outcome_service_url = $serviceurl;
$message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'} = $basicoutcome;
}
$member->message = [$message];
}
foreach ($includedcapabilities as $capabilityname => $capability) {
if (($capability['type'] === 'id') || $isallowedlticonfig[$capability['type']]) {
$member->{$capability['member.field']} = $capability['source.value'];
}
}
$arrusers['members'][] = $member;
}
if ($more) {
$nextlimitfrom = $limitfrom + $limitnum;
$nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}";
if (!is_null($lti)) {
$nextpage .= "&rlid={$lti->id}";
}
$response->add_additional_header("Link: <{$nextpage}>; rel=\"next\"");
}
$response->set_content_type('application/vnd.ims.lti-nrps.v2.membershipcontainer+json');
return json_encode($arrusers);
}
/**
* Determines whether a user attribute may be used as part of LTI membership
* @param array $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 = isset($toolconfig[$field]) && (self::ALWAYS_INCLUDE_FIELD == $toolconfig[$field]);
if (!$allowed && isset($toolconfig[$field]) && (self::DELEGATE_TO_INSTRUCTOR == $toolconfig[$field]) &&
!is_null($instanceconfig)) {
$allowed = isset($instanceconfig->{"lti_{$field}"}) &&
($instanceconfig->{"lti_{$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 = $this->get_component_id();
$options = [
get_string('notallow', $this->get_component_id()),
get_string('allow', $this->get_component_id())
];
$mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options);
$mform->setType($elementname, 'int');
$mform->setDefault($elementname, 0);
$mform->addHelpButton($elementname, $elementname, $this->get_component_id());
}
/**
* 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->{$this->get_component_id()})) {
if ($tool->{$this->get_component_id()} == parent::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) {
$launchparameters['context_memberships_url'] = '$ToolProxyBinding.memberships.url';
$launchparameters['context_memberships_v2_url'] = '$ToolProxyBinding.memberships.url';
$launchparameters['context_memberships_versions'] = '1.0,2.0';
}
}
return $launchparameters;
}
/**
* Return an array of key/claim mapping allowing LTI 1.1 custom parameters
* to be transformed to LTI 1.3 claims.
*
* @return array Key/value pairs of params to claim mapping.
*/
public function get_jwt_claim_mappings(): array {
return [
'custom_context_memberships_v2_url' => [
'suffix' => 'nrps',
'group' => 'namesroleservice',
'claim' => 'context_memberships_url',
'isarray' => false
],
'custom_context_memberships_versions' => [
'suffix' => 'nrps',
'group' => 'namesroleservice',
'claim' => 'service_versions',
'isarray' => true
]
];
}
}
@@ -0,0 +1,114 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_memberships.
*
* @package ltiservice_memberships
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_memberships\privacy;
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_memberships.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->link_external_location('External LTI provider.', [
'userid' => 'privacy:metadata:userid',
'useridnumber' => 'privacy:metadata:useridnumber',
'fullname' => 'privacy:metadata:fullname',
'firstname' => 'privacy:metadata:firstname',
'lastname' => 'privacy:metadata:lastname',
'email' => 'privacy:metadata:email'
], 'privacy:metadata:externalpurpose');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all user data which matches the specified context.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
}
@@ -0,0 +1,37 @@
<?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_memberships', language 'en'
*
* @package ltiservice_memberships
* @copyright 2015 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @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 Names and Role Provisioning';
$string['ltiservice_memberships_help'] = 'Allow the tool to retrieve members\' info from the course using the IMS LTI Names and Role Provisioning Service. The privacy settings will apply. For course-level requests these will be based on the tool configuration settings. If you wish to always send such details, do not delegate the choice to teachers. Link-level requests will always use the privacy settings which apply to the link.';
$string['notallow'] = 'Do not use this service';
$string['pluginname'] = 'Names and Role Provisioning LTI Service';
$string['privacy:metadata:email'] = 'The email of the user using the LTI consumer.';
$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
$string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI consumer.';
$string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI consumer.';
$string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.';
$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
$string['privacy:metadata:useridnumber'] = 'The ID number of the user using the LTI consumer';
@@ -0,0 +1,52 @@
<?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/>.
/**
* Unit tests for ltiservice_memberships privacy provider.
*
* @package ltiservice_memberships
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_memberships\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\tests\provider_testcase;
/**
* Unit tests for ltiservice_memberships privacy provider.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Basic setup for these tests.
*/
public function setUp(): void {
$this->resetAfterTest(true);
}
/**
* Test getting the context for the user ID related to this plugin.
*/
public function test_get_contexts_for_userid(): void {
$user = $this->getDataGenerator()->create_user();
$contextlist = \ltiservice_memberships\privacy\provider::get_contexts_for_userid($user->id);
$this->assertEmpty($contextlist);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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_memberships service.
*
* @package ltiservice_memberships
* @copyright 2015 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_memberships';
@@ -0,0 +1,224 @@
<?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 Tool Consumer Profile resource
*
* @package ltiservice_profile
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_profile\local\resources;
use \mod_lti\local\ltiservice\service_base;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing the Tool Consumer Profile.
*
* @package ltiservice_profile
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class profile extends \mod_lti\local\ltiservice\resource_base {
/**
* Class constructor.
*
* @param service_base $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'ToolConsumerProfile';
$this->template = '/profile/{tool_proxy_id}';
$this->variables[] = 'ToolConsumerProfile.url';
$this->formats[] = 'application/vnd.ims.lti.v2.toolconsumerprofile+json';
$this->methods[] = 'GET';
}
/**
* Get the path for this resource.
*
* @return string
*/
public function get_path() {
$path = $this->template;
$toolproxy = $this->get_service()->get_tool_proxy();
if (!empty($toolproxy)) {
$path = str_replace('{tool_proxy_id}', $toolproxy->guid, $path);
}
return $path;
}
/**
* Execute the request for this resource.
*
* @param \mod_lti\local\ltiservice\response $response Response object for this request.
*/
public function execute($response) {
global $CFG;
$version = service_base::LTI_VERSION2P0;
$params = $this->parse_template();
if (optional_param('lti_version', service_base::LTI_VERSION2P0, PARAM_ALPHANUMEXT) != $version) {
$ok = false;
$response->set_code(400);
} else {
$toolproxy = lti_get_tool_proxy_from_guid($params['tool_proxy_id']);
$ok = $toolproxy !== false;
}
if ($ok) {
$this->get_service()->set_tool_proxy($toolproxy);
$response->set_content_type($this->formats[0]);
$servicepath = $this->get_service()->get_service_path();
$id = $servicepath . $this->get_path();
$now = date('Y-m-d\TH:iO');
$capabilityofferedarr = explode("\n", $toolproxy->capabilityoffered);
$serviceofferedarr = explode("\n", $toolproxy->serviceoffered);
$serviceoffered = '';
$sep = '';
$services = \core_component::get_plugin_list('ltiservice');
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();
foreach ($resources as $resource) {
$formats = implode("\", \"", $resource->get_formats());
$methods = implode("\", \"", $resource->get_methods());
$capabilityofferedarr = array_merge($capabilityofferedarr, $resource->get_variables());
$template = $resource->get_path();
if (!empty($template)) {
$path = $servicepath . preg_replace('/[\(\)]/', '', $template);
} else {
$path = $resource->get_endpoint();
}
$serviceoffered .= <<< EOD
{$sep}
{
"@type":"{$resource->get_type()}",
"@id":"tcp:{$resource->get_id()}",
"endpoint":"{$path}",
"format":["{$formats}"],
"action":["{$methods}"]
}
EOD;
$sep = ',';
}
}
}
$capabilityoffered = implode("\",\n \"", $capabilityofferedarr);
if (strlen($capabilityoffered) > 0) {
$capabilityoffered = "\n \"{$capabilityoffered}\"";
}
$urlparts = parse_url($CFG->wwwroot);
$orgid = $urlparts['host'];
$name = 'Moodle';
$code = 'moodle';
$vendorname = 'Moodle.org';
$vendorcode = 'mdl';
$prodversion = strval($CFG->version);
if (!empty($CFG->mod_lti_institution_name)) {
$consumername = $CFG->mod_lti_institution_name;
$consumerdesc = '';
} else {
$consumername = get_site()->fullname;
$consumerdesc = strip_tags(get_site()->summary);
}
$profile = <<< EOD
{
"@context":[
"http://purl.imsglobal.org/ctx/lti/v2/ToolConsumerProfile",
{
"tcp":"{$id}#"
}
],
"@type":"ToolConsumerProfile",
"@id":"{$id}",
"lti_version":"{$version}",
"guid":"{$toolproxy->guid}",
"product_instance":{
"guid":"{$orgid}",
"product_info":{
"product_name":{
"default_value":"{$name}",
"key":"product.name"
},
"product_version":"{$prodversion}",
"product_family":{
"code":"{$code}",
"vendor":{
"code":"{$vendorcode}",
"vendor_name":{
"default_value":"{$vendorname}",
"key":"product.vendor.name"
},
"timestamp":"{$now}"
}
}
},
"service_owner":{
"@id":"ServiceOwner",
"service_owner_name":{
"default_value":"{$consumername}",
"key":"service_owner.name"
},
"description":{
"default_value":"{$consumerdesc}",
"key":"service_owner.description"
}
}
},
"capability_offered":[{$capabilityoffered}
],
"service_offered":[{$serviceoffered}
]
}
EOD;
$response->set_body($profile);
}
}
/**
* Parse a value for custom parameter substitution variables.
*
* @param string $value String to be parsed
*
* @return string
*/
public function parse_value($value) {
if (!empty($this->get_service()->get_tool_proxy()) && (strpos($value, '$ToolConsumerProfile.url') !== false)) {
$value = str_replace('$ToolConsumerProfile.url', $this->get_endpoint(), $value);
}
return $value;
}
}
@@ -0,0 +1,69 @@
<?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 Tool Consumer Profile service
*
* @package ltiservice_profile
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_profile\local\service;
defined('MOODLE_INTERNAL') || die();
/**
* A service implementing the Tool Consumer Profile.
*
* @package ltiservice_profile
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class profile extends \mod_lti\local\ltiservice\service_base {
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'profile';
$this->name = 'Tool Consumer Profile';
$this->unsigned = true;
}
/**
* Get the resources for this service.
*
* @return array
*/
public function get_resources() {
if (empty($this->resources)) {
$this->resources = array();
$this->resources[] = new \ltiservice_profile\local\resources\profile($this);
}
return $this->resources;
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_profile.
*
* @package ltiservice_profile
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_profile\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_profile implementing null_provider.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,27 @@
<?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_profile', language 'en'
*
* @package ltiservice_profile
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Tool Consumer Profile LTI Service';
$string['privacy:metadata'] = 'The Tool Consumer Profile LTI Service plugin does not store any personal data.';
+32
View File
@@ -0,0 +1,32 @@
<?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_profile service.
*
* @package ltiservice_profile
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_profile';
+28
View File
@@ -0,0 +1,28 @@
// 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 all necessary code to initiate a tool registration process
*
* @package mod_lti
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
LTI Service plugins directory
==============================
This directory contains LTI service plugins which are discoverable from the Tool
Consumer Profile.
@@ -0,0 +1,312 @@
<?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 Tool Proxy resource
*
* @package ltiservice_toolproxy
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolproxy\local\resources;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/lti/OAuth.php');
require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
// TODO: Switch to core oauthlib once implemented - MDL-30149.
use moodle\mod\lti as lti;
/**
* A resource implementing the Tool Proxy.
*
* @package ltiservice_toolproxy
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class toolproxy extends \mod_lti\local\ltiservice\resource_base {
/**
* Class constructor.
*
* @param ltiservice_toolproxy\local\resources\toolproxy $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'ToolProxy.collection';
$this->template = '/toolproxy';
$this->formats[] = 'application/vnd.ims.lti.v2.toolproxy+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) {
$ok = $this->check_tool(null, $response->get_request_data());
$ok = $ok && ($this->get_service()->get_tool_proxy());
if ($ok) {
$toolproxy = $this->get_service()->get_tool_proxy();
}
if (!$ok) {
$toolproxy = null;
$response->set_code(401);
}
$tools = array();
// Ensure all required elements are present in the Tool Proxy.
if ($ok) {
$toolproxyjson = json_decode($response->get_request_data());
$ok = !empty($toolproxyjson);
if (!$ok) {
debugging('Tool proxy is not properly formed JSON');
} else {
$ok = isset($toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code);
$ok = $ok && isset($toolproxyjson->security_contract->shared_secret);
$ok = $ok && isset($toolproxyjson->tool_profile->resource_handler);
if (!$ok) {
debugging('One or more missing elements from tool proxy: vendor code, shared secret or resource handlers');
}
}
}
// Check all capabilities requested were offered.
if ($ok) {
$offeredcapabilities = explode("\n", $toolproxy->capabilityoffered);
$resources = $toolproxyjson->tool_profile->resource_handler;
$errors = array();
foreach ($resources as $resource) {
if (isset($resource->message)) {
foreach ($resource->message as $message) {
if (!in_array($message->message_type, $offeredcapabilities)) {
$errors[] = $message->message_type;
} else if (isset($resource->parameter)) {
foreach ($message->parameter as $parameter) {
if (isset($parameter->variable) && !in_array($parameter->variable, $offeredcapabilities)) {
$errors[] = $parameter->variable;
}
}
}
}
}
}
if (count($errors) > 0) {
$ok = false;
debugging('Tool proxy contains capabilities which were not offered: ' . implode(', ', $errors));
}
}
// Check all services requested were offered (only tool services currently supported).
$requestsbasicoutcomes = false;
if ($ok && isset($toolproxyjson->security_contract->tool_service)) {
$contexts = lti_get_contexts($toolproxyjson);
$profileservice = lti_get_service_by_name('profile');
$profileservice->set_tool_proxy($toolproxy);
$context = $profileservice->get_service_path() . $profileservice->get_resources()[0]->get_path() . '#';
$offeredservices = explode("\n", $toolproxy->serviceoffered);
$services = lti_get_services();
$tpservices = $toolproxyjson->security_contract->tool_service;
$errors = array();
foreach ($tpservices as $service) {
$fqid = lti_get_fqid($contexts, $service->service);
$requestsbasicoutcomes = $requestsbasicoutcomes || (substr($fqid, -13) === 'Outcomes.LTI1');
if (substr($fqid, 0, strlen($context)) !== $context) {
$errors[] = $service->service;
} else {
$id = explode('#', $fqid, 2);
$aservice = lti_get_service_by_resource_id($services, $id[1]);
$classname = explode('\\', get_class($aservice));
if (empty($aservice) || !in_array($classname[count($classname) - 1], $offeredservices)) {
$errors[] = $service->service;
}
}
}
if (count($errors) > 0) {
$ok = false;
debugging('Tool proxy contains services which were not offered: ' . implode(', ', $errors));
}
}
// Extract all launchable tools from the resource handlers.
if ($ok) {
$resources = $toolproxyjson->tool_profile->resource_handler;
$messagetypes = [
'basic-lti-launch-request',
'ContentItemSelectionRequest',
];
foreach ($resources as $resource) {
$launchable = false;
$messages = array();
$tool = new \stdClass();
$iconinfo = null;
if (is_array($resource->icon_info)) {
$iconinfo = $resource->icon_info[0];
} else {
$iconinfo = $resource->icon_info;
}
if (isset($iconinfo) && isset($iconinfo->default_location) && isset($iconinfo->default_location->path)) {
$tool->iconpath = $iconinfo->default_location->path;
}
foreach ($resource->message as $message) {
if (in_array($message->message_type, $messagetypes)) {
$launchable = $launchable || ($message->message_type === 'basic-lti-launch-request');
$messages[$message->message_type] = $message;
}
}
if (!$launchable) {
continue;
}
$tool->name = $resource->resource_name->default_value;
$tool->messages = $messages;
$tools[] = $tool;
}
$ok = count($tools) > 0;
if (!$ok) {
debugging('No launchable messages found in tool proxy');
}
}
// Add tools and custom parameters.
if ($ok) {
$baseurl = '';
if (isset($toolproxyjson->tool_profile->base_url_choice[0]->default_base_url)) {
$baseurl = $toolproxyjson->tool_profile->base_url_choice[0]->default_base_url;
}
$securebaseurl = '';
if (isset($toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url)) {
$securebaseurl = $toolproxyjson->tool_profile->base_url_choice[0]->secure_base_url;
}
foreach ($tools as $tool) {
$messages = $tool->messages;
$launchrequest = $messages['basic-lti-launch-request'];
$config = new \stdClass();
$config->lti_toolurl = "{$baseurl}{$launchrequest->path}";
$config->lti_typename = $tool->name;
$config->lti_coursevisible = 1;
$config->lti_forcessl = 0;
if (isset($messages['ContentItemSelectionRequest'])) {
$contentitemrequest = $messages['ContentItemSelectionRequest'];
$config->lti_contentitem = 1;
if ($launchrequest->path !== $contentitemrequest->path) {
$config->lti_toolurl_ContentItemSelectionRequest = $baseurl . $contentitemrequest->path;
}
$contentitemcapabilities = implode("\n", $contentitemrequest->enabled_capability);
$config->lti_enabledcapability_ContentItemSelectionRequest = $contentitemcapabilities;
$contentitemparams = self::lti_extract_parameters($contentitemrequest->parameter);
$config->lti_parameter_ContentItemSelectionRequest = $contentitemparams;
}
$type = new \stdClass();
$type->state = LTI_TOOL_STATE_PENDING;
$type->ltiversion = LTI_VERSION_2;
$type->toolproxyid = $toolproxy->id;
// Ensure gradebook column is created.
if ($requestsbasicoutcomes && !in_array('BasicOutcome.url', $launchrequest->enabled_capability)) {
$launchrequest->enabled_capability[] = 'BasicOutcome.url';
}
if ($requestsbasicoutcomes && !in_array('BasicOutcome.sourcedId', $launchrequest->enabled_capability)) {
$launchrequest->enabled_capability[] = 'BasicOutcome.sourcedId';
}
$type->enabledcapability = implode("\n", $launchrequest->enabled_capability);
$type->parameter = self::lti_extract_parameters($launchrequest->parameter);
if (!empty($tool->iconpath)) {
$type->icon = "{$baseurl}{$tool->iconpath}";
if (!empty($securebaseurl)) {
$type->secureicon = "{$securebaseurl}{$tool->iconpath}";
}
}
$ok = $ok && (lti_add_type($type, $config) !== false);
}
if (isset($toolproxyjson->custom)) {
lti_set_tool_settings($toolproxyjson->custom, $toolproxy->id);
}
}
if (!empty($toolproxy)) {
if ($ok) {
// If all went OK accept the tool proxy.
$toolproxy->state = LTI_TOOL_PROXY_STATE_ACCEPTED;
$toolproxy->toolproxy = $response->get_request_data();
$toolproxy->secret = $toolproxyjson->security_contract->shared_secret;
$toolproxy->vendorcode = $toolproxyjson->tool_profile->product_instance->product_info->product_family->vendor->code;
$url = $this->get_endpoint();
$body = <<< EOD
{
"@context" : "http://purl.imsglobal.org/ctx/lti/v2/ToolProxyId",
"@type" : "ToolProxy",
"@id" : "{$url}",
"tool_proxy_guid" : "{$toolproxy->guid}"
}
EOD;
$response->set_code(201);
$response->set_content_type('application/vnd.ims.lti.v2.toolproxy.id+json');
$response->set_body($body);
} else {
// Otherwise reject the tool proxy.
$toolproxy->state = LTI_TOOL_PROXY_STATE_REJECTED;
$response->set_code(400);
}
lti_update_tool_proxy($toolproxy);
} else {
$response->set_code(400);
}
}
/**
* Extracts the message parameters from the tool proxy entry
*
* @param array $parameters Parameter section of a message
*
* @return String containing parameters
*/
private static function lti_extract_parameters($parameters) {
$params = array();
foreach ($parameters as $parameter) {
if (isset($parameter->variable)) {
$value = '$' . $parameter->variable;
} else {
$value = $parameter->fixed;
if (strlen($value) > 0) {
$first = substr($value, 0, 1);
if (($first == '$') || ($first == '\\')) {
$value = '\\' . $value;
}
}
}
$params[] = "{$parameter->name}={$value}";
}
return implode("\n", $params);
}
}
@@ -0,0 +1,68 @@
<?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 Tool Proxy service
*
* @package ltiservice_toolproxy
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolproxy\local\service;
defined('MOODLE_INTERNAL') || die();
/**
* A service implementing the Tool Proxy.
*
* @package ltiservice_toolproxy
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class toolproxy extends \mod_lti\local\ltiservice\service_base {
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'toolproxy';
$this->name = 'Tool Proxy';
}
/**
* Get the resources for this service.
*
* @return array
*/
public function get_resources() {
if (empty($this->resources)) {
$this->resources = array();
$this->resources[] = new \ltiservice_toolproxy\local\resources\toolproxy($this);
}
return $this->resources;
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_toolproxy.
*
* @package ltiservice_toolproxy
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolproxy\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_toolproxy implementing null_provider.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,27 @@
<?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_toolproxy', language 'en'
*
* @package ltiservice_toolproxy
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Tool Proxy Service';
$string['privacy:metadata'] = 'The Tool Proxy Service plugin does not store any personal data.';
+35
View File
@@ -0,0 +1,35 @@
<?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_toolproxy service.
*
* @package ltiservice_toolproxy
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_toolproxy';
$plugin->dependencies = [
'ltiservice_profile' => 2024041600,
];
@@ -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 a class definition for the Context Settings resource
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolsettings\local\resources;
use ltiservice_toolsettings\local\service\toolsettings;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing the Context-level (ToolProxyBinding) Settings.
*
* @package ltiservice_toolsettings
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contextsettings extends \mod_lti\local\ltiservice\resource_base {
/**
* Class constructor.
*
* @param \mod_lti\local\ltiservice\service_base $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'ToolProxyBindingSettings';
$this->template = '/{context_type}/{context_id}/bindings/{vendor_code}/{product_code}(/custom)';
$this->variables[] = 'ToolProxyBinding.custom.url';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
$this->methods[] = 'GET';
$this->methods[] = 'PUT';
}
/**
* Execute the request for this resource.
*
* @param \mod_lti\local\ltiservice\response $response Response object for this request.
*/
public function execute($response) {
$params = $this->parse_template();
$contexttype = $params['context_type'];
$contextid = $params['context_id'];
$vendorcode = $params['vendor_code'];
$productcode = $params['product_code'];
$bubble = optional_param('bubble', '', PARAM_ALPHA);
$typeid = null;
if (($vendorcode === 'tool') && is_numeric($productcode)) {
$typeid = $productcode;
}
$ok = !empty($contexttype) && !empty($contextid) &&
!empty($vendorcode) && !empty($productcode) &&
$this->check_tool($typeid, $response->get_request_data(),
array(toolsettings::SCOPE_TOOL_SETTINGS));
if (!$ok) {
$response->set_code(401);
} else {
$toolproxy = $this->get_service()->get_tool_proxy();
if (!empty($toolproxy)) {
$ok = $toolproxy->guid === $productcode;
$typeid = null;
$id = $toolproxy->id;
} else {
$ok = $vendorcode === 'tool';
$typeid = intval($productcode);
$id = -$typeid;
}
$contenttype = $response->get_accept();
$simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
if ($ok) {
$ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
(!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
(empty($bubble) || ($response->get_request_method() == 'GET'));
}
if (!$ok) {
$response->set_code(404);
} else {
$systemsetting = null;
$contextsettings = lti_get_tool_settings($id, $contextid);
if (!empty($bubble)) {
$systemsetting = new systemsettings($this->get_service());
$systemsetting->params['tool_proxy_id'] = $productcode;
if ($id >= 0) {
$systemsetting->params['config_type'] = 'toolproxy';
} else {
$systemsetting->params['config_type'] = 'tool';
}
$systemsettings = lti_get_tool_settings($id);
if ($bubble == 'distinct') {
toolsettings::distinct_settings($systemsettings, $contextsettings, null);
}
} else {
$systemsettings = null;
}
if ($response->get_request_method() == 'GET') {
$json = '';
if ($simpleformat) {
$response->set_content_type($this->formats[1]);
$json .= "{";
} else {
$response->set_content_type($this->formats[0]);
$json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n";
}
$settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting);
$json .= $settings;
$isfirst = strlen($settings) <= 0;
$settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $this);
if ((strlen($settings) > 0) && !$isfirst) {
$json .= ",";
}
$json .= $settings;
if ($simpleformat) {
$json .= "\n}";
} else {
$json .= "\n ]\n}";
}
$response->set_body($json);
} else { // PUT.
$settings = null;
if ($response->get_content_type() == $this->formats[0]) {
$json = json_decode($response->get_request_data());
$ok = !empty($json);
if ($ok) {
$ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) &&
($json->{"@graph"}[0]->{"@type"} == 'ToolProxyBinding');
}
if ($ok) {
$settings = $json->{"@graph"}[0]->custom;
unset($settings->{'@id'});
}
} else { // Simple JSON.
$json = json_decode($response->get_request_data(), true);
$ok = !empty($json);
if ($ok) {
$ok = is_array($json);
}
if ($ok) {
$settings = $json;
}
}
if ($ok) {
lti_set_tool_settings($settings, $id, $contextid);
} else {
$response->set_code(406);
}
}
}
}
}
/**
* 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, '$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;
if (!empty($this->get_service()->get_tool_proxy())) {
$this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
$this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid;
} else {
$this->params['vendor_code'] = 'tool';
$this->params['product_code'] = $this->get_service()->get_type()->id;
}
$value = str_replace('$ToolProxyBinding.custom.url', parent::get_endpoint(), $value);
}
return $value;
}
}
@@ -0,0 +1,221 @@
<?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 Context Settings resource
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolsettings\local\resources;
use ltiservice_toolsettings\local\service\toolsettings;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing the Context-level (ToolProxyBinding) Settings.
*
* @package ltiservice_toolsettings
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class linksettings extends \mod_lti\local\ltiservice\resource_base {
/**
* Class constructor.
*
* @param \mod_lti\local\ltiservice\service_base $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'LtiLinkSettings';
$this->template = '/links/{link_id}(/custom)';
$this->variables[] = 'LtiLink.custom.url';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
$this->methods[] = 'GET';
$this->methods[] = 'PUT';
}
/**
* Execute the request for this resource.
*
* @param \mod_lti\local\ltiservice\response $response Response object for this request.
*/
public function execute($response) {
global $DB, $COURSE;
$params = $this->parse_template();
$linkid = $params['link_id'];
$bubble = optional_param('bubble', '', PARAM_ALPHA);
$contenttype = $response->get_accept();
$simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
$ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
(!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
(empty($bubble) || ($response->get_request_method() == self::HTTP_GET));
if (!$ok) {
$response->set_code(406);
}
$systemsetting = null;
$contextsetting = null;
$lti = null;
if ($ok) {
$ok = !empty($linkid);
if ($ok) {
$lti = $DB->get_record('lti', array('id' => $linkid), 'course,typeid', MUST_EXIST);
$ok = $this->check_tool($lti->typeid, $response->get_request_data(),
array(toolsettings::SCOPE_TOOL_SETTINGS));
}
if (!$ok) {
$response->set_code(401);
}
}
if ($ok) {
if (!empty($this->get_service()->get_tool_proxy())) {
$id = $this->get_service()->get_tool_proxy()->id;
} else {
$id = -$this->get_service()->get_type()->id;
}
if ($response->get_request_method() == 'GET') {
$linksettings = lti_get_tool_settings($id, $lti->course, $linkid);
if (!empty($bubble)) {
$contextsetting = new contextsettings($this->get_service());
if ($COURSE == 'site') {
$contextsetting->params['context_type'] = 'Group';
} else {
$contextsetting->params['context_type'] = 'CourseSection';
}
$contextsetting->params['context_id'] = $lti->course;
if ($id >= 0) {
$contextsetting->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
} else {
$contextsetting->params['vendor_code'] = 'tool';
}
$contextsetting->params['product_code'] = abs($id);
$contextsettings = lti_get_tool_settings($id, $lti->course);
$systemsetting = new systemsettings($this->get_service());
if ($id >= 0) {
$systemsetting->params['config_type'] = 'toolproxy';
} else {
$systemsetting->params['config_type'] = 'tool';
}
$systemsetting->params['tool_proxy_id'] = abs($id);
$systemsettings = lti_get_tool_settings($id);
if ($bubble == 'distinct') {
toolsettings::distinct_settings($systemsettings, $contextsettings, $linksettings);
}
} else {
$contextsettings = null;
$systemsettings = null;
}
$json = '';
if ($simpleformat) {
$response->set_content_type($this->formats[1]);
$json .= "{";
} else {
$response->set_content_type($this->formats[0]);
$json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n";
}
$settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting);
$json .= $settings;
$isfirst = strlen($settings) <= 0;
$settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $contextsetting);
if (strlen($settings) > 0) {
if (!$isfirst) {
$json .= ",";
if (!$simpleformat) {
$json .= "\n";
}
}
$isfirst = false;
}
$json .= $settings;
$settings = toolsettings::settings_to_json($linksettings, $simpleformat, 'LtiLink', $this);
if ((strlen($settings) > 0) && !$isfirst) {
$json .= ",";
if (!$simpleformat) {
$json .= "\n";
}
}
$json .= $settings;
if ($simpleformat) {
$json .= "\n}";
} else {
$json .= "\n ]\n}";
}
$response->set_body($json);
} else { // PUT.
$settings = null;
if ($response->get_content_type() == $this->formats[0]) {
$json = json_decode($response->get_request_data());
$ok = !empty($json);
if ($ok) {
$ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) &&
($json->{"@graph"}[0]->{"@type"} == 'LtiLink');
}
if ($ok) {
$settings = $json->{"@graph"}[0]->custom;
unset($settings->{'@id'});
}
} else { // Simple JSON.
$json = json_decode($response->get_request_data(), true);
$ok = !empty($json);
if ($ok) {
$ok = is_array($json);
}
if ($ok) {
$settings = $json;
}
}
if ($ok) {
lti_set_tool_settings($settings, $id, $lti->course, $linkid);
} else {
$response->set_code(406);
}
}
}
}
/**
* Parse a value for custom parameter substitution variables.
*
* @param string $value String to be parsed
*
* @return string
*/
public function parse_value($value) {
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);
}
return $value;
}
}
@@ -0,0 +1,171 @@
<?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 System Settings resource
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolsettings\local\resources;
use ltiservice_toolsettings\local\service\toolsettings;
use mod_lti\local\ltiservice\resource_base;
defined('MOODLE_INTERNAL') || die();
/**
* A resource implementing the System-level (ToolProxy) Settings.
*
* @package ltiservice_toolsettings
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class systemsettings extends resource_base {
/**
* Class constructor.
*
* @param \mod_lti\local\ltiservice\service_base $service Service instance
*/
public function __construct($service) {
parent::__construct($service);
$this->id = 'ToolProxySettings';
$this->template = '/{config_type}/{tool_proxy_id}(/custom)';
$this->variables[] = 'ToolProxy.custom.url';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
$this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
$this->methods[] = self::HTTP_GET;
$this->methods[] = self::HTTP_PUT;
}
/**
* Execute the request for this resource.
*
* @param \mod_lti\local\ltiservice\response $response Response object for this request.
*/
public function execute($response) {
$params = $this->parse_template();
$tpid = $params['tool_proxy_id'];
$configtype = $params['config_type'];
$ok = (in_array($configtype, array('toolproxy', 'tool')));
if ($ok) {
$typeid = null;
if (($configtype === 'tool') && is_numeric($tpid)) {
$typeid = $tpid;
}
$bubble = optional_param('bubble', '', PARAM_ALPHA);
$ok = !empty($tpid) && $this->check_tool($typeid, $response->get_request_data(),
array(toolsettings::SCOPE_TOOL_SETTINGS));
}
if (!$ok) {
$response->set_code(401);
} else if (!empty($this->get_service()->get_tool_proxy())) {
$ok = ($this->get_service()->get_tool_proxy()->guid === $tpid);
$id = $this->get_service()->get_tool_proxy()->id;
} else if (!empty($typeid)) {
$id = -$typeid;
} else {
$ok = false;
$response->set_code(404);
}
$contenttype = $response->get_accept();
$simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
if ($ok) {
$ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
(!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
(empty($bubble) || ($response->get_request_method() == 'GET'));
if (!$ok) {
$response->set_code(406);
}
}
if ($ok) {
$systemsettings = lti_get_tool_settings($id);
if ($response->get_request_method() == 'GET') {
$json = '';
if ($simpleformat) {
$response->set_content_type($this->formats[1]);
$json .= "{";
} else {
$response->set_content_type($this->formats[0]);
$json .= "{\n \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n \"@graph\":[\n";
}
$json .= toolsettings::settings_to_json($systemsettings, $simpleformat,
'ToolProxy', $this);
if ($simpleformat) {
$json .= "\n}";
} else {
$json .= "\n ]\n}";
}
$response->set_body($json);
} else { // PUT.
$settings = null;
if ($response->get_content_type() == $this->formats[0]) {
$json = json_decode($response->get_request_data());
$ok = !empty($json);
if ($ok) {
$ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) &&
($json->{"@graph"}[0]->{"@type"} == 'ToolProxy');
}
if ($ok) {
$settings = $json->{"@graph"}[0]->custom;
unset($settings->{'@id'});
}
} else { // Simple JSON.
$json = json_decode($response->get_request_data(), true);
$ok = !empty($json);
if ($ok) {
$ok = is_array($json);
}
if ($ok) {
$settings = $json;
}
}
if ($ok) {
lti_set_tool_settings($settings, $id);
} else {
$response->set_code(406);
}
}
}
}
/**
* Parse a value for custom parameter substitution variables.
*
* @param string $value String to be parsed
*
* @return string
*/
public function parse_value($value) {
if (strpos($value, '$ToolProxy.custom.url') !== false) {
$value = str_replace('$ToolProxy.custom.url', parent::get_endpoint(), $value);
}
return $value;
}
}
@@ -0,0 +1,216 @@
<?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 Tool Settings service
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolsettings\local\service;
defined('MOODLE_INTERNAL') || die();
/**
* A service implementing Tool Settings.
*
* @package ltiservice_toolsettings
* @since Moodle 2.8
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class toolsettings extends \mod_lti\local\ltiservice\service_base {
/** Scope for managing tool settings */
const SCOPE_TOOL_SETTINGS = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting';
/**
* Class constructor.
*/
public function __construct() {
parent::__construct();
$this->id = 'toolsettings';
$this->name = 'Tool Settings';
}
/**
* Get the resources for this service.
*
* @return array
*/
public function get_resources() {
if (empty($this->resources)) {
$this->resources = array();
$this->resources[] = new \ltiservice_toolsettings\local\resources\systemsettings($this);
$this->resources[] = new \ltiservice_toolsettings\local\resources\contextsettings($this);
$this->resources[] = new \ltiservice_toolsettings\local\resources\linksettings($this);
}
return $this->resources;
}
/**
* Get the scope(s) permitted for the tool relevant to this service.
*
* @return array
*/
public function get_permitted_scopes() {
$scopes = array();
$ok = !empty($this->get_type());
if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) &&
($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) {
$scopes[] = self::SCOPE_TOOL_SETTINGS;
}
return $scopes;
}
/**
* Get the scope(s) defined this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_TOOL_SETTINGS];
}
/**
* Get the distinct settings from each level by removing any duplicates from higher levels.
*
* @param array $systemsettings System level settings
* @param array $contextsettings Context level settings
* @param array $linksettings Link level settings
*/
public static function distinct_settings(&$systemsettings, &$contextsettings, $linksettings) {
if (!empty($systemsettings)) {
foreach ($systemsettings as $key => $value) {
if ((!empty($contextsettings) && array_key_exists($key, $contextsettings)) ||
(!empty($linksettings) && array_key_exists($key, $linksettings))) {
unset($systemsettings[$key]);
}
}
}
if (!empty($contextsettings)) {
foreach ($contextsettings as $key => $value) {
if (!empty($linksettings) && array_key_exists($key, $linksettings)) {
unset($contextsettings[$key]);
}
}
}
}
/**
* Get the JSON representation of the settings.
*
* @param array $settings Settings
* @param boolean $simpleformat <code>true</code> if simple JSON is to be returned
* @param string $type JSON-LD type
* @param \mod_lti\local\ltiservice\resource_base $resource Resource handling the request
*
* @return string
*/
public static function settings_to_json($settings, $simpleformat, $type, $resource) {
$json = '';
if (!empty($resource)) {
$indent = '';
if (!$simpleformat) {
$json .= " {\n \"@type\":\"{$type}\",\n";
$json .= " \"@id\":\"{$resource->get_endpoint()}\",\n";
$json .= " \"custom\":{";
$indent = ' ';
}
$isfirst = true;
if (!empty($settings)) {
foreach ($settings as $key => $value) {
if (!$isfirst) {
$json .= ',';
} else {
$isfirst = false;
}
$json .= "\n{$indent} \"{$key}\":\"{$value}\"";
}
}
if (!$simpleformat) {
$json .= "\n{$indent}}\n }";
}
}
return $json;
}
/**
* Adds form elements for membership add/edit page.
*
* @param \MoodleQuickForm $mform
*/
public function get_configuration_options(&$mform) {
$elementname = $this->get_component_id();
$options = [
get_string('notallow', $this->get_component_id()),
get_string('allow', $this->get_component_id())
];
$mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options);
$mform->setType($elementname, 'int');
$mform->setDefault($elementname, 0);
$mform->addHelpButton($elementname, $elementname, $this->get_component_id());
}
/**
* 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->{$this->get_component_id()})) {
if ($tool->{$this->get_component_id()} == self::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) {
$launchparameters['system_setting_url'] = '$ToolProxy.custom.url';
$launchparameters['context_setting_url'] = '$ToolProxyBinding.custom.url';
if ($messagetype === 'basic-lti-launch-request') {
$launchparameters['link_setting_url'] = '$LtiLink.custom.url';
}
}
}
return $launchparameters;
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for ltiservice_toolsettings.
*
* @package ltiservice_toolsettings
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace ltiservice_toolsettings\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for ltiservice_toolsettings implementing null_provider.
*
* @copyright 2018 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,31 @@
<?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_toolsettings', language 'en'
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['allow'] = 'Use this service';
$string['ltiservice_toolsettings'] = 'Tool Settings';
$string['ltiservice_toolsettings_help'] = 'Allow the tool to save and retrieve setting values.';
$string['notallow'] = 'Do not use this service';
$string['pluginname'] = 'Tool Settings Service';
$string['privacy:metadata'] = 'The Tool Settings Service plugin does not store any personal data.';
+36
View File
@@ -0,0 +1,36 @@
<?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_toolsettings service.
*
* @package ltiservice_toolsettings
* @copyright 2014 Vital Source Technologies http://vitalsource.com
* @author Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'ltiservice_toolsettings';
$plugin->dependencies = [
'ltiservice_profile' => 2024041600,
'ltiservice_toolproxy' => 2024041600,
];