Commit 6489a3d4 authored by Marina Glancy's avatar Marina Glancy

MDL-34161 mod_lti: backup/restore of lti types

Only course tools are backed up, site tools and registrations
can be matched by id if they are restored to the same site only.

For predefined course tools the secret is backed up encrypted
and can be restored on the same site only.
parent edbcee25
......@@ -53,7 +53,12 @@ defined('MOODLE_INTERNAL') || die;
*/
class backup_lti_activity_structure_step extends backup_activity_structure_step {
/**
* Defines structure of activity backup
* @return backup_nested_element
*/
protected function define_structure() {
global $DB;
// To know if we are including userinfo.
$userinfo = $this->get_setting_value('userinfo');
......@@ -87,50 +92,90 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step
)
);
$ltitypes = new backup_nested_element('ltitypes');
$ltitype = new backup_nested_element('ltitype', array('id'), array(
$ltitype = new backup_nested_element('ltitype', array('id'), array(
'name',
'baseurl',
'tooldomain',
'state',
'course',
'coursevisible',
'toolproxyid',
'enabledcapability',
'parameter',
'icon',
'secureicon',
'createdby',
'timecreated',
'timemodified',
'description'
)
);
$ltitypesconfigs = new backup_nested_element('ltitypesconfigs');
$ltitypesconfig = new backup_nested_element('ltitypesconfig', array('id'), array(
'typeid',
'name',
'value',
'name',
'value',
)
);
$ltitypesconfigencrypted = new backup_nested_element('ltitypesconfigencrypted', array('id'), array(
'name',
new encrypted_final_element('value'),
)
);
$ltitoolproxy = new backup_nested_element('ltitoolproxy', array('id'));
$ltitoolsettings = new backup_nested_element('ltitoolsettings');
$ltitoolsetting = new backup_nested_element('ltitoolsetting', array('id'), array(
'settings',
'timecreated',
'timemodified',
)
);
// Build the tree
$lti->add_child($ltitypes);
$lti->add_child($ltitypesconfigs);
$ltitypes->add_child($ltitype);
$lti->add_child($ltitype);
$ltitype->add_child($ltitypesconfigs);
$ltitypesconfigs->add_child($ltitypesconfig);
$ltitypesconfigs->add_child($ltitypesconfigencrypted);
$ltitype->add_child($ltitoolproxy);
$ltitoolproxy->add_child($ltitoolsettings);
$ltitoolsettings->add_child($ltitoolsetting);
// Define sources.
$lti->set_source_table('lti', array('id' => backup::VAR_ACTIVITYID));
$ltitype->set_source_sql("SELECT lt.*
FROM {lti} l
JOIN {lti_types} lt ON lt.id = l.typeid
WHERE l.id = ?", array(backup::VAR_ACTIVITYID));
$ltitypesconfig->set_source_sql("SELECT lc.*
FROM {lti} l
JOIN {lti_types_config} lc ON lc.typeid = l.typeid
WHERE lc.name != 'password'
AND lc.name != 'resourcekey'
AND lc.name != 'servicesalt'
AND l.id = ?", array(backup::VAR_ACTIVITYID));
$ltirecord = $DB->get_record('lti', ['id' => $this->task->get_activityid()]);
$lti->set_source_array([$ltirecord]);
$ltitypedata = $this->retrieve_lti_type($ltirecord);
$ltitype->set_source_array($ltitypedata ? [$ltitypedata] : []);
if (isset($ltitypedata->baseurl)) {
// Add type config values only if the type was backed up. Encrypt password and resourcekey.
$params = [backup_helper::is_sqlparam($ltitypedata->id),
backup_helper::is_sqlparam('password'),
backup_helper::is_sqlparam('resourcekey')];
$ltitypesconfig->set_source_sql("SELECT id, name, value
FROM {lti_types_config}
WHERE typeid = ? AND name <> ? AND name <> ?", $params);
$ltitypesconfigencrypted->set_source_sql("SELECT id, name, value
FROM {lti_types_config}
WHERE typeid = ? AND (name = ? OR name = ?)", $params);
}
if (!empty($ltitypedata->toolproxyid)) {
// If this is LTI 2 tool add settings for the current activity.
$ltitoolproxy->set_source_array([['id' => $ltitypedata->toolproxyid]]);
$ltitoolsetting->set_source_sql("SELECT *
FROM {lti_tool_settings}
WHERE toolproxyid = ? AND course = ? AND coursemoduleid = ?",
[backup_helper::is_sqlparam($ltitypedata->toolproxyid), backup::VAR_COURSEID, backup::VAR_MODID]);
} else {
$ltitoolproxy->set_source_array([]);
}
// Define id annotations
$ltitype->annotate_ids('user', 'createdby');
$ltitype->annotate_ids('course', 'course');
// Define file annotations.
$lti->annotate_files('mod_lti', 'intro', null); // This file areas haven't itemid.
......@@ -142,4 +187,34 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step
// Return the root element (lti), wrapped into standard activity structure.
return $this->prepare_activity_structure($lti);
}
/**
* Retrieves a record from {lti_type} table associated with the current activity
*
* Information about site tools is not returned because it is insecure to back it up,
* only fields necessary for same-site tool matching are left in the record
*
* @param stdClass $ltirecord record from {lti} table
* @return stdClass|null
*/
protected function retrieve_lti_type($ltirecord) {
global $DB;
if (!$ltirecord->typeid) {
return null;
}
$record = $DB->get_record('lti_types', ['id' => $ltirecord->typeid]);
if ($record && $record->course == SITEID) {
// Site LTI types or registrations are not backed up except for their name (which is visible).
// Predefined course types can be backed up.
$allowedkeys = ['id', 'course', 'name', 'toolproxyid'];
foreach ($record as $key => $value) {
if (!in_array($key, $allowedkeys)) {
$record->$key = null;
}
}
}
return $record;
}
}
......@@ -53,13 +53,20 @@ defined('MOODLE_INTERNAL') || die;
*/
class restore_lti_activity_structure_step extends restore_activity_structure_step {
/** @var bool */
protected $newltitype = false;
protected function define_structure() {
$paths = array();
$lti = new restore_path_element('lti', '/activity/lti');
$paths[] = new restore_path_element('ltitype', '/activity/lti/ltitypes/ltitype');
$paths[] = new restore_path_element('ltitypesconfig', '/activity/lti/ltitypesconfigs/ltitypesconfig');
$paths[] = $lti;
$paths[] = new restore_path_element('ltitype', '/activity/lti/ltitype');
$paths[] = new restore_path_element('ltitypesconfig', '/activity/lti/ltitype/ltitypesconfigs/ltitypesconfig');
$paths[] = new restore_path_element('ltitypesconfigencrypted',
'/activity/lti/ltitype/ltitypesconfigs/ltitypesconfigencrypted');
$paths[] = new restore_path_element('ltitoolproxy', '/activity/lti/ltitype/ltitoolproxy');
$paths[] = new restore_path_element('ltitoolsetting', '/activity/lti/ltitype/ltitoolproxy/ltitoolsettings/ltitoolsetting');
// Add support for subplugin structures.
$this->add_subplugin_structure('ltisource', $lti);
......@@ -98,65 +105,93 @@ class restore_lti_activity_structure_step extends restore_activity_structure_ste
/**
* Process an lti type restore
* @param object $data The data in object form
* @param mixed $data The data from backup XML file
* @return void
*/
protected function process_ltitype($data) {
global $DB;
global $DB, $USER;
$data = (object)$data;
$data->createdby = $this->get_mappingid('user', $data->createdby);
$ltitype = $DB->get_record_sql("SELECT *
FROM {lti_types}
WHERE id = ?
AND baseurl = ?", array($data->id, $data->baseurl));
// If restore is occurring on the same site, don't add lti_types data if
// restoring on the SITEID. If restore isn't occurring on the same site,
// always add lti_type data from backup.
if ($this->task->is_samesite() && $ltitype->course != SITEID
&& $ltitype->state == LTI_TOOL_STATE_CONFIGURED) {
// If restoring into the same course, use existing data, else re-create.
$courseid = $this->get_courseid();
if ($ltitype->course != $courseid) {
// Override course field of restore data with current courseid.
$data->course = $courseid;
$ltitype = new stdClass();
$ltitype->id = $DB->insert_record('lti_types', $data);
}
} else if (!$this->task->is_samesite() || !isset($ltitype->id)) {
// Either we are restoring into a new site, or didn't find a database match.
// Override course field of restore data with current courseid.
$data->course = $this->get_courseid();
$ltitype = new stdClass();
$ltitype->id = $DB->insert_record('lti_types', $data);
$oldid = $data->id;
if (!empty($data->createdby)) {
$data->createdby = $this->get_mappingid('user', $data->createdby) ?: $USER->id;
}
$courseid = $this->get_courseid();
$data->course = ($this->get_mappingid('course', $data->course) == $courseid) ? $courseid : SITEID;
// Try to find existing lti type with the same properties.
$ltitypeid = $this->find_existing_lti_type($data);
$this->newltitype = false;
if (!$ltitypeid && $data->course == $courseid) {
unset($data->toolproxyid); // Course tools can not use LTI2.
$ltitypeid = $DB->insert_record('lti_types', $data);
$this->newltitype = true;
$this->set_mapping('ltitype', $oldid, $ltitypeid);
}
// Add the typeid entry back to LTI module.
$lti = new stdClass();
$lti->id = $this->get_new_parentid('lti');
$lti->typeid = $ltitype->id;
$DB->update_record('lti', $lti);
$DB->update_record('lti', ['id' => $this->get_new_parentid('lti'), 'typeid' => $ltitypeid]);
}
/**
* Attempts to find existing record in lti_type
* @param stdClass $data
* @return int|null field lti_types.id or null if tool is not found
*/
protected function find_existing_lti_type($data) {
global $DB;
if ($ltitypeid = $this->get_mappingid('ltitype', $data->id)) {
return $ltitypeid;
}
$ltitype = null;
$params = (array)$data;
if ($this->task->is_samesite()) {
// If we are restoring on the same site try to find lti type with the same id.
$sql = 'id = :id AND course = :course';
$sql .= ($data->toolproxyid) ? ' AND toolproxyid = :toolproxyid' : ' AND toolproxyid IS NULL';
if ($DB->record_exists_select('lti_types', $sql, $params)) {
$this->set_mapping('ltitype', $data->id, $data->id);
if ($data->toolproxyid) {
$this->set_mapping('ltitoolproxy', $data->toolproxyid, $data->toolproxyid);
}
return $data->id;
}
}
if ($data->course != $this->get_courseid()) {
// Site tools are not backed up and are not restored.
return null;
}
// Now try to find the same type on the current site available in this course.
// Compare only fields baseurl, course and name, if they are the same we assume it is the same tool.
// LTI2 is not possible in the course so we add "lt.toolproxyid IS NULL" to the query.
$sql = 'SELECT id
FROM {lti_types}
WHERE baseurl = :baseurl AND course = :course AND name = :name AND toolproxyid IS NULL';
if ($ltitype = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE)) {
$this->set_mapping('ltitype', $data->id, $ltitype->id);
return $ltitype->id;
}
return null;
}
/**
* Process an lti config restore
* @param object $data The data in object form
* @return void
* @param mixed $data The data from backup XML file
*/
protected function process_ltitypesconfig($data) {
global $DB;
$data = (object)$data;
$data->typeid = $this->get_new_parentid('ltitype');
$parentid = $this->get_new_parentid('lti');
$lti = $DB->get_record_sql("SELECT typeid
FROM {lti}
WHERE id = ?", array($parentid));
// Only add configuration if typeid doesn't match new LTI tool.
if ($lti->typeid != $data->typeid) {
$data->typeid = $lti->typeid;
// Only add configuration if the new lti_type was created.
if ($data->typeid && $this->newltitype) {
if ($data->name == 'servicesalt') {
$data->value = uniqid('', true);
}
......@@ -164,6 +199,53 @@ class restore_lti_activity_structure_step extends restore_activity_structure_ste
}
}
/**
* Process an lti config restore
* @param mixed $data The data from backup XML file
*/
protected function process_ltitypesconfigencrypted($data) {
global $DB;
$data = (object)$data;
$data->typeid = $this->get_new_parentid('ltitype');
// Only add configuration if the new lti_type was created.
if ($data->typeid && $this->newltitype) {
$data->value = $this->decrypt($data->value);
if (!is_null($data->value)) {
$DB->insert_record('lti_types_config', $data);
}
}
}
/**
* Process a restore of LTI tool registration
* This method is empty because we actually process registration as part of process_ltitype()
* @param mixed $data The data from backup XML file
*/
protected function process_ltitoolproxy($data) {
}
/**
* Process an lti tool registration settings restore (only settings for the current activity)
* @param mixed $data The data from backup XML file
*/
protected function process_ltitoolsetting($data) {
global $DB;
$data = (object)$data;
$data->toolproxyid = $this->get_new_parentid('ltitoolproxy');
if (!$data->toolproxyid) {
return;
}
$data->course = $this->get_courseid();
$data->coursemoduleid = $this->task->get_moduleid();
$DB->insert_record('lti_tool_settings', $data);
}
protected function after_execute() {
// Add lti related files, no need to match by itemname (just internally handled context).
$this->add_related_files('mod_lti', 'intro', null);
......
......@@ -55,6 +55,10 @@ Feature: Add preconfigured tools via teacher interface
And the field "Secure icon URL" matches value "https://download.moodle.org/unittest/test.jpg"
And I press "Cancel"
And I switch to the main window
And I press "Save and return to course"
And I open "Test tool activity 1" actions menu
And I choose "Edit settings" in the open action menu
And the field "Preconfigured tool" matches value "Placeholder"
@javascript @_switch_window
Scenario: Add and use a preconfigured tool
......
@mod @mod_lti @core_backup @javascript
Feature: Restoring Moodle 2 backup restores LTI configuration
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Terry1 | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
| Course 2 | C2 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher1 | C2 | editingteacher |
Scenario: Backup and restore course with preconfigured site LTI tool on the same site
When I log in as "admin"
And I navigate to "Manage tools" node in "Site administration > Plugins > Activity modules > External tool"
And I follow "Manage preconfigured tools"
And I follow "Add preconfigured tool"
And I set the following fields to these values:
| Tool name | My site tool |
| Tool URL | https://www.moodle.org |
| lti_coursevisible | 1 |
And I press "Save changes"
And I navigate to "Manage tools" node in "Site administration > Plugins > Activity modules > External tool"
And "This tool has not yet been used" "text" should exist in the "//div[contains(@id,'tool-card-container') and contains(., 'My site tool')]" "xpath_element"
And I am on site homepage
And I follow "Course 1"
And I turn editing mode on
And I add a "External tool" to section "1" and I fill the form with:
| Activity name | My LTI module |
| Preconfigured tool | My site tool |
| Launch container | Embed |
And I follow "Course 1"
And I should see "My LTI module"
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
And I am on site homepage
And I follow "Course 1 copy 1"
And I open "My LTI module" actions menu
And I choose "Edit settings" in the open action menu
Then the field "Preconfigured tool" matches value "My site tool"
And I navigate to "Manage tools" node in "Site administration > Plugins > Activity modules > External tool"
And "This tool is being used 2 times" "text" should exist in the "//div[contains(@id,'tool-card-container') and contains(., 'My site tool')]" "xpath_element"
@javascript @_switch_window
Scenario: Backup and restore course with preconfigured course LTI tool on the same site
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
# In the first course create an LTI module that uses a course preconfigured toolю
And I add a "External tool" to section "1"
And I set the following fields to these values:
| Activity name | Test tool activity 2 |
And I follow "Add preconfigured tool"
And I switch to "add_tool" window
And I set the field "Tool name" to "My course tool"
And I set the field "Tool URL" to "http://www.example.com/lti/provider.php"
And I set the field "Consumer key" to "my key"
And I set the field "Shared secret" to "my secret"
And I set the field "Default launch container" to "Existing window"
And I press "Save changes"
And I switch to the main window
And I press "Save and return to course"
# Backup course and restore into another course
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into "Course 2" course using this options:
And I am on site homepage
And I follow "Course 2"
# Make sure the copy of the preconfigured tool was created in the second course with both encrtypted and non-encrypted properties.
And I open "Test tool activity 2" actions menu
And I choose "Edit settings" in the open action menu
Then the field "Preconfigured tool" matches value "My course tool"
And I follow "Edit preconfigured tool"
And I switch to "edit_tool" window
Then the field "Tool URL" matches value "http://www.example.com/lti/provider.php"
And the field "Consumer key" matches value "my key"
And the field "Shared secret" matches value "my secret"
And the field "Default launch container" matches value "Existing window"
And I press "Cancel"
And I switch to the main window
@mod @mod_lti @core_backup @javascript
Feature: Restoring Moodle 2 backup restores LTI configuration
Background:
Given the following "courses" exist:
| fullname | shortname | category | idnumber |
| Course 1 | C1 | 0 | C1 |
And I log in as "admin"
Scenario: Backup and restore course 1
Given I navigate to "Manage external tool types" node in "Site administration > Plugins > Activity modules > LTI"
And I follow "Add external tool configuration"
And I set the following fields to these values:
| Tool name | Site Netspot Tool |
| Tool base URL | http://www.netspot.com.au |
| lti_coursevisible | 1 |
And I press "Save changes"
And I wait to be redirected
Given I am on site homepage
Then I follow "Course 1"
Then I turn editing mode on
And I add a "External tool" to section "1" and I fill the form with:
| Activity name | Site Netspot Tool |
| External tool type | Site Netspot Tool |
| Launch container | Embed |
And I follow "Course 1"
Then I should see "Site Netspot Tool"
Then I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
Then I am on site homepage
And I follow "Course 1 copy 1"
And I open "Site Netspot Tool" actions menu
And I click on "Edit settings" "link" in the "Site Netspot Tool" activity
Then the field "External tool type" matches value "Site Netspot Tool"
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment