From e4913b7433690c8137dc04d1e2c9858dcd951f61 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 9 Jan 2025 11:01:23 +1100 Subject: [PATCH 1/4] MDL-81864 core_ltix: Implement backup/restore for core_ltix --- backup/moodle2/backup_activity_task.class.php | 5 + backup/moodle2/backup_block_task.class.php | 7 +- backup/moodle2/backup_course_task.class.php | 6 + backup/moodle2/backup_root_task.class.php | 5 + backup/moodle2/backup_settingslib.php | 9 + backup/moodle2/backup_stepslib.php | 240 +++++++++++++++ .../moodle2/restore_activity_task.class.php | 7 +- backup/moodle2/restore_block_task.class.php | 5 + backup/moodle2/restore_course_task.class.php | 8 +- backup/moodle2/restore_root_task.class.php | 12 + backup/moodle2/restore_settingslib.php | 9 + backup/moodle2/restore_stepslib.php | 275 ++++++++++++++++++ lang/en/backup.php | 1 + 13 files changed, 586 insertions(+), 3 deletions(-) diff --git a/backup/moodle2/backup_activity_task.class.php b/backup/moodle2/backup_activity_task.class.php index 9f3c632deae..cf81f269e00 100644 --- a/backup/moodle2/backup_activity_task.class.php +++ b/backup/moodle2/backup_activity_task.class.php @@ -224,6 +224,11 @@ abstract class backup_activity_task extends backup_task { $this->add_step(new backup_xapistate_structure_step('activity_xapistate', 'xapistate.xml')); } + // Generate the ltixs file (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new backup_ltixs_structure_step('activity_ltixs', 'ltixs.xml')); + } + // At the end, mark it as built $this->built = true; } diff --git a/backup/moodle2/backup_block_task.class.php b/backup/moodle2/backup_block_task.class.php index 66ccbd54f34..84c6e62ecda 100644 --- a/backup/moodle2/backup_block_task.class.php +++ b/backup/moodle2/backup_block_task.class.php @@ -165,7 +165,12 @@ abstract class backup_block_task extends backup_task { $this->add_step(new backup_comments_structure_step('block_comments', 'comments.xml')); } - // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) + // Generate the ltixs file (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new backup_ltixs_structure_step('block_ltixs', 'ltixs.xml')); + } + + // Generate the inforef file (must be after ALL steps gathering annotations of ANY type). $this->add_step(new backup_inforef_structure_step('block_inforef', 'inforef.xml')); // Migrate the already exported inforef entries to final ones diff --git a/backup/moodle2/backup_course_task.class.php b/backup/moodle2/backup_course_task.class.php index 2b937d60ad6..a1f2ed71ba4 100644 --- a/backup/moodle2/backup_course_task.class.php +++ b/backup/moodle2/backup_course_task.class.php @@ -142,6 +142,12 @@ class backup_course_task extends backup_task { $this->add_step(new backup_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml')); } + // Generate the ltixtypes and ltixs files (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new backup_ltixtypes_structure_step('ltix_types', 'ltixtypes.xml')); + $this->add_step(new backup_ltixs_structure_step('course_ltixs', 'ltixs.xml')); + } + // At the end, mark it as built $this->built = true; } diff --git a/backup/moodle2/backup_root_task.class.php b/backup/moodle2/backup_root_task.class.php index aa30a9d3015..252e7232a08 100644 --- a/backup/moodle2/backup_root_task.class.php +++ b/backup/moodle2/backup_root_task.class.php @@ -192,6 +192,11 @@ class backup_root_task extends backup_task { $this->add_setting($xapistate); $users->add_dependency($xapistate); + // Define LTI tool settings inclusion setting. + $ltix = new backup_ltix_setting('ltix', base_setting::IS_BOOLEAN, true); + $ltix->set_ui(new backup_setting_ui_checkbox($ltix, get_string('rootsettingltix', 'backup'))); + $this->add_setting($ltix); + // Define legacy file inclusion setting. $legacyfiles = new backup_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, true); $legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup'))); diff --git a/backup/moodle2/backup_settingslib.php b/backup/moodle2/backup_settingslib.php index 4b96aad42f6..ef97444cd10 100644 --- a/backup/moodle2/backup_settingslib.php +++ b/backup/moodle2/backup_settingslib.php @@ -296,3 +296,12 @@ class backup_contentbankcontent_setting extends backup_generic_setting { */ class backup_xapistate_setting extends backup_generic_setting { } + +/** + * Root setting to control if backup will include LTIX settings or not. + * + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_ltix_setting extends backup_generic_setting { +} diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 2f620905da0..79fec1aa1cd 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -3146,3 +3146,243 @@ class backup_xapistate_structure_step extends backup_structure_step { return $states; } } + +/** + * Structure step in charge of constructing the ltixtypes.xml file for all the LTI types. + * + * @copyright 2025 Shamim Rezaei + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_ltixtypes_structure_step extends backup_structure_step { + #[\Override] + protected function define_structure() { + $ltitypes = new backup_nested_element('ltitypes'); + $ltitype = new backup_nested_element('ltitype', ['id'], [ + 'name', + 'baseurl', + 'tooldomain', + 'state', + 'course', + 'coursevisible', + 'ltiversion', + 'clientid', + 'toolproxyid', + 'enabledcapability', + 'parameter', + 'icon', + 'secureicon', + 'createdby', + 'timecreated', + 'timemodified', + 'description', + ]); + + $ltitypesconfigs = new backup_nested_element('ltitypesconfigs'); + $ltitypesconfig = new backup_nested_element('ltitypesconfig', ['id'], [ + 'name', + 'value', + ]); + $ltitypesconfigencrypted = new backup_nested_element('ltitypesconfigencrypted', ['id'], [ + 'name', + new encrypted_final_element('value'), + ]); + + $ltitoolproxy = new backup_nested_element('ltitoolproxy', ['id']); + + $ltitoolsettings = new backup_nested_element('ltitoolsettings'); + $ltitoolsetting = new backup_nested_element('ltitoolsetting', ['id'], [ + 'settings', + 'timecreated', + 'timemodified', + ]); + + $lticoursevisible = new backup_nested_element('lticoursevisible', ['id'], [ + 'coursevisible', + ]); + + // Build the tree. + $ltitypes->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); + $ltitype->add_child($lticoursevisible); + + // Define sources. + $ltitypearray = $this->retrieve_lti_types(); + $ltitype->set_source_array($ltitypearray ?: []); + + // Add type config values only if the type is not a site type. Encrypt password and resourcekey. + $params = [ + 'typeid' => backup::VAR_PARENTID, + 'siteid' => backup_helper::is_sqlparam(SITEID), + 'password' => backup_helper::is_sqlparam('password'), + 'resourcekey' => backup_helper::is_sqlparam('resourcekey'), + ]; + $ltitypesconfig->set_source_sql( + "SELECT ltc.id, ltc.name, ltc.value + FROM {lti_types_config} ltc + JOIN {lti_types} lt ON lt.id = ltc.typeid AND lt.course <> :siteid + WHERE ltc.typeid = :typeid + AND ltc.name <> :password + AND ltc.name <> :resourcekey", + $params + ); + $ltitypesconfigencrypted->set_source_sql( + "SELECT ltc.id, ltc.name, ltc.value + FROM {lti_types_config} ltc + JOIN {lti_types} lt ON lt.id = ltc.typeid AND lt.course <> :siteid + WHERE ltc.typeid = :typeid + AND (ltc.name = :password OR ltc.name = :resourcekey)", + $params + ); + + // If this is LTI 2 tool add settings for the current activity. + $ltitoolproxy->set_source_sql( + "SELECT toolproxyid AS id FROM {lti_types} WHERE id = ? AND toolproxyid IS NOT NULL", + [backup::VAR_PARENTID] + ); + $ltitoolsetting->set_source_sql( + "SELECT * + FROM {lti_tool_settings} + WHERE toolproxyid = ? AND course = ?", + [backup::VAR_PARENTID, backup::VAR_COURSEID]); + + $lticoursevisible->set_source_sql( + "SELECT id, coursevisible FROM {lti_coursevisible} WHERE typeid = ? AND courseid = ?", + [backup::VAR_PARENTID, backup::VAR_COURSEID] + ); + + // Define id annotations. + $ltitype->annotate_ids('user', 'createdby'); + $ltitype->annotate_ids('course', 'course'); + + // Attach ltixsource plugin structure to $ltitype element, only one allowed. + $this->add_plugin_structure('ltixsource', $ltitype, false); + // Attach ltixservice plugin structure to $ltitype element, only one allowed. + $this->add_plugin_structure('ltixservice', $ltitype, false); + + return $ltitypes; + } + + /** + * Retrieve records from {lti_type} table associated with the course. + * + * 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 + * + * @return stdClass[] + */ + protected function retrieve_lti_types(): array { + global $DB; + + $params = [ + 'courseid' => $this->get_courseid(), + 'coursecontextlevel' => CONTEXT_COURSE, + 'slashlike' => '/%', + ]; + $pathmatch = $DB->sql_like('c_rl.path', $DB->sql_concat('c_course.path', ':slashlike')); + $records = $DB->get_records_sql( + "SELECT DISTINCT t.* + FROM {lti_types} t + JOIN {lti_resource_link} rl ON rl.typeid = t.id + JOIN {context} c_rl ON c_rl.id = rl.contextid + JOIN {context} c_course ON c_course.instanceid = :courseid + AND c_course.contextlevel = :coursecontextlevel + WHERE c_rl.path = c_course.path OR $pathmatch", + $params + ); + + foreach ($records as $record) { + if ($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 $records; + } +} + +/** + * Structure step in charge of constructing the ltixs.xml file for all the LTI resource links. + * + * @copyright 2025 Shamim Rezaei + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_ltixs_structure_step extends backup_structure_step { + #[\Override] + protected function define_structure() { + // To know if we are including userinfo. + if ($this->setting_exists('userinfo')) { + $userinfo = $this->get_setting_value('userinfo'); + } else if ($this->setting_exists('users')) { + $userinfo = $this->get_setting_value('users'); + } else { + // Default to true if the setting does not exist. + $userinfo = true; + } + + // Define each element separated. + $resourcelinks = new backup_nested_element('ltiresourcelinks'); + $resourcelink = new backup_nested_element('ltiresourcelink', ['id'], [ + 'typeid', + 'component', + 'itemtype', + 'itemid', + 'contextid', + 'url', + 'title', + 'text', + 'textformat', + 'gradable', + 'launchcontainer', + 'customparams', + 'icon', + ]); + + $ltisubmissions = new backup_nested_element('ltisubmissions'); + $ltisubmission = new backup_nested_element('ltisubmission', ['id'], [ + 'userid', + 'datesubmitted', + 'dateupdated', + 'gradepercent', + 'originalgrade', + 'launchid', + 'state', + ]); + + // Build the tree. + $resourcelinks->add_child($resourcelink); + $resourcelink->add_child($ltisubmissions); + $ltisubmissions->add_child($ltisubmission); + + // Define sources. + $resourcelink->set_source_table('lti_resource_link', ['contextid' => backup::VAR_CONTEXTID]); + + // All the rest of elements only happen if we are including user info. + if ($userinfo) { + // TODO: The following need to be updated to not use `ltiid`. I assumed it refers to the lti_resource_link. + $ltisubmission->set_source_table('lti_submission', ['ltiid' => backup::VAR_PARENTID]); + } + + // Define id annotations. + //$resourcelink->annotate_ids('lti_types', 'typeid'); + // We don't need to annotate itemid here. It will be mapped to the correct itemid during restore. + $ltisubmission->annotate_ids('user', 'userid'); + + // Add support for ltix plugin structures. + $this->add_plugin_structure('ltixsource', $resourcelink, false); + $this->add_plugin_structure('ltixservice', $resourcelink, true); + + return $resourcelinks; + } +} diff --git a/backup/moodle2/restore_activity_task.class.php b/backup/moodle2/restore_activity_task.class.php index 9ed5e0df6da..d7e2b5aad2e 100644 --- a/backup/moodle2/restore_activity_task.class.php +++ b/backup/moodle2/restore_activity_task.class.php @@ -214,7 +214,12 @@ abstract class restore_activity_task extends restore_task { $this->add_step(new restore_xapistate_structure_step('activity_xapistate', 'xapistate.xml')); } - // At the end, mark it as built + // The ltixs (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new restore_ltixs_structure_step('activity_ltixs', 'ltixs.xml')); + } + + // At the end, mark it as built. $this->built = true; } diff --git a/backup/moodle2/restore_block_task.class.php b/backup/moodle2/restore_block_task.class.php index 9c82d43e65b..f3821e32980 100644 --- a/backup/moodle2/restore_block_task.class.php +++ b/backup/moodle2/restore_block_task.class.php @@ -98,6 +98,11 @@ abstract class restore_block_task extends restore_task { $this->add_step(new restore_comments_structure_step('block_comments', 'comments.xml')); } + // The ltixs (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new restore_ltixs_structure_step('block_ltixs', 'ltixs.xml')); + } + // Search reindexing (if enabled). if (\core_search\manager::is_indexing_enabled()) { $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE; diff --git a/backup/moodle2/restore_course_task.class.php b/backup/moodle2/restore_course_task.class.php index d8a99b05edb..fe94d5fade2 100644 --- a/backup/moodle2/restore_course_task.class.php +++ b/backup/moodle2/restore_course_task.class.php @@ -133,7 +133,13 @@ class restore_course_task extends restore_task { $this->add_step(new restore_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml')); } - // At the end, mark it as built + // The ltixtypes and ltixs files (conditionally). + if ($this->get_setting_value('ltix')) { + $this->add_step(new restore_ltixtypes_structure_step('ltix_types', 'ltixtypes.xml')); + $this->add_step(new restore_ltixs_structure_step('course_ltixs', 'ltixs.xml')); + } + + // At the end, mark it as built. $this->built = true; } diff --git a/backup/moodle2/restore_root_task.class.php b/backup/moodle2/restore_root_task.class.php index b3c31cf246b..d16d8e10f56 100644 --- a/backup/moodle2/restore_root_task.class.php +++ b/backup/moodle2/restore_root_task.class.php @@ -340,6 +340,18 @@ class restore_root_task extends restore_task { $xapistate->get_ui()->set_changeable($changeable); $this->add_setting($xapistate); + // Define LTIx types and instances. + $defaultvalue = false; + $changeable = false; + if (isset($rootsettings['ltix']) && $rootsettings['ltix']) { // Only enabled when available. + $defaultvalue = true; + $changeable = true; + } + $ltix = new restore_ltix_setting('ltix', base_setting::IS_BOOLEAN, $defaultvalue); + $ltix->set_ui(new backup_setting_ui_checkbox($ltix, get_string('rootsettingltix', 'backup'))); + $ltix->get_ui()->set_changeable($changeable); + $this->add_setting($ltix); + // Include legacy files. $defaultvalue = true; $changeable = true; diff --git a/backup/moodle2/restore_settingslib.php b/backup/moodle2/restore_settingslib.php index f62a7e433c7..5e83f33cb9a 100644 --- a/backup/moodle2/restore_settingslib.php +++ b/backup/moodle2/restore_settingslib.php @@ -322,3 +322,12 @@ class restore_contentbankcontent_setting extends restore_generic_setting { */ class restore_xapistate_setting extends restore_generic_setting { } + +/** + * Root setting to control if restore will create LTIX settings or not. + * + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_ltix_setting extends restore_generic_setting { +} diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 83e7ea8fcd1..aacbf32cb8b 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -6771,3 +6771,278 @@ class restore_calendar_action_events extends restore_execution_step { \core\task\manager::queue_adhoc_task($task, true); } } + +/** + * Structure step in charge of restoring the ltixtypes.xml file + * + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_ltixtypes_structure_step extends restore_structure_step { + + /** @var bool Whether a new LTI type is being created during the restore process */ + protected bool $newltitype = false; + + /** + * Finds an existing LTI type during a restoration process. + * Depending on the site being restored to and the course matching criteria, + * it either maps the existing LTI type or returns null if no match is found. + * + * @param stdClass $data An object containing details of the LTI type being restored. + * @return int|null Returns the ID of the matched LTI type if found, otherwise null. + */ + protected function find_existing_lti_type(stdClass $data): ?int { + global $DB; + + if ($ltitypeid = $this->get_mappingid('ltitype', $data->id)) { + return $ltitypeid; + } + + $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. + $compareclause = $DB->sql_compare_text('baseurl', 255) . ' = ' . $DB->sql_compare_text(':baseurl', 255); + $sql = "SELECT id + FROM {lti_types} + WHERE $compareclause 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; + } + + #[\Override] + protected function define_structure() { + $paths = []; + $ltitype = new restore_path_element('ltitype', '/ltitypes/ltitype'); + $paths[] = $ltitype; + $paths[] = new restore_path_element('ltitypesconfig', '/ltitypes/ltitype/ltitypesconfigs/ltitypesconfig'); + $paths[] = new restore_path_element('ltitypesconfigencrypted', '/ltitypes/ltitype/ltitypesconfigs/ltitypesconfigencrypted'); + $paths[] = new restore_path_element('ltitoolproxy', '/ltitypes/ltitype/ltitoolproxy'); + $paths[] = new restore_path_element('ltitoolsetting', '/ltitypes/ltitype/ltitoolproxy/ltitoolsettings/ltitoolsetting'); + $paths[] = new restore_path_element('lticoursevisible', '/ltitypes/ltitype/lticoursevisible'); + + // Add support for ltix plugin structures. + $this->add_plugin_structure('ltixsource', $ltitype); + $this->add_plugin_structure('ltixservice', $ltitype); + + return $paths; + } + + /** + * Process an lti type restore + * @param array $data The data from the backup XML file + */ + protected function process_ltitype(array $data): void { + global $DB, $USER; + + $data = (object) $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 the 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. + if (!empty($data->clientid)) { + // Need to rebuild clientid to ensure uniqueness. + $data->clientid = \core_ltix\local\ltiopenid\registration_helper::get()->new_clientid(); + } + $ltitypeid = $DB->insert_record('lti_types', $data); + $this->newltitype = true; + $this->set_mapping('ltitype', $oldid, $ltitypeid); + } + } + + /** + * Process an lti config restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltitypesconfig(array $data): void { + 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) { + if ($data->name == 'servicesalt') { + $data->value = uniqid('', true); + } + $DB->insert_record('lti_types_config', $data); + } + } + + /** + * Process an lti config restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltitypesconfigencrypted(array $data): void { + 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 array $data The data from the backup XML file + */ + protected function process_ltitoolproxy(array $data): void {} + + /** + * Process an lti tool registration settings restore (only settings for the current activity) + * + * @param array $data The data from the backup XML file + */ + protected function process_ltitoolsetting(array $data): void { + global $DB; + + $data = (object) $data; + $data->toolproxyid = $this->get_new_parentid('ltitoolproxy'); + + if (!$data->toolproxyid) { + return; + } + + $data->course = $this->get_courseid(); + // TODO: The `coursemoduleid` field in the `lti_tool_settings` table should change. I'm not sure what to set it to. + $data->coursemoduleid = null; + $DB->insert_record('lti_tool_settings', $data); + } + + /** + * Process an lti coursevisible restore + * + * @param array $data The data from the backup XML file + */ + protected function process_lticoursevisible(array $data): void { + global $DB; + + $data = (object) $data; + $data->typeid = $this->get_new_parentid('ltitype'); + $data->courseid = $this->get_courseid(); + + if ($data->typeid) { + $DB->insert_record('lti_coursevisible', $data); + } + } +} + +/** + * Restore structure step for LTI resource links. + * + * This class is responsible for restoring LTI resource links and their associated submissions. + * + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_ltixs_structure_step extends restore_structure_step { + #[\Override] + protected function define_structure() { + $paths = []; + // To know if we are including userinfo. + if ($this->setting_exists('userinfo')) { + $userinfo = $this->get_setting_value('userinfo'); + } else if ($this->setting_exists('users')) { + $userinfo = $this->get_setting_value('users'); + } else { + // Default to true if the setting does not exist. + $userinfo = true; + } + + $resourcelink = new restore_path_element('ltiresourcelink', '/ltiresourcelinks/ltiresourcelink'); + $paths[] = $resourcelink; + $paths[] = new restore_path_element('ltisubmission', '/ltiresourcelinks/ltiresourcelink/ltisubmissions/ltisubmission'); + + // Add support for ltix plugin structures. + $this->add_plugin_structure('ltixsource', $resourcelink); + $this->add_plugin_structure('ltixservice', $resourcelink); + + return $paths; + } + + /** + * Process an lti resource link restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltiresourcelink(array $data): void { + global $DB; + + $data = (object) $data; + $oldid = $data->id; + + $data->typeid = $this->get_mappingid('ltitype', $data->typeid); + $mapping = $this->task->get_ltiresourcelink_mapping_itemname($data->itemtype); + $data->itemid = $this->get_mappingid($mapping, $data->itemid); + $data->contextid = $this->get_mappingid('context', $data->contextid); + $data->servicesalt = uniqid('', true); + + $resourcelinkid = $DB->insert_record('lti_resource_link', $data); + $this->set_mapping('ltiresourcelink', $oldid, $resourcelinkid); + } + + /** + * Process an lti submission restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltisubmission(array $data): void { + global $DB; + + $data = (object) $data; + + // TODO: The following need to be updated to not use `ltiid`. I assumed it refers to the lti_resource_link table. + $data->ltiid = $this->get_new_parentid('ltiresourcelink'); + + if ($data->userid > 0) { + $data->userid = $this->get_mappingid('user', $data->userid); + } + + $data->datesubmitted = $this->apply_date_offset($data->datesubmitted); + $data->dateupdated = $this->apply_date_offset($data->dateupdated); + + $DB->insert_record('lti_submission', $data); + } +} diff --git a/lang/en/backup.php b/lang/en/backup.php index 57d622281eb..171ab8a956d 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -390,6 +390,7 @@ $string['rootsettinggroups'] = 'Include groups and groupings'; $string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0'; $string['rootsettingimscc11'] = 'Convert to IMS Common Cartridge 1.1'; $string['rootsettingxapistate'] = 'Include user\'s state in content such as H5P activities'; +$string['rootsettingltix'] = 'Include LTI usages'; $string['samesitenotification'] = 'This backup was created with only references to files, not the files themselves. Restoring will only work on this site.'; $string['section_prefix'] = 'Section {$a}: '; $string['sitecourseformatwarning'] = 'This is a site home backup. It can only be restored on the site home.'; -- 2.39.5 (Apple Git-154) From 7f3fa6c4dfc78f0e5a40060b0cfe1e4e305f7ff8 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Tue, 21 Jan 2025 05:41:13 +1100 Subject: [PATCH 2/4] MDL-81864 ltixservice_gradebookservices: Imlement backup/restore --- ...vice_gradebookservices_subplugin.class.php | 140 -------------- ...service_gradebookservices_plugin.class.php | 126 ++++++++++++ ...ervice_gradebookservices_plugin.class.php} | 183 ++++++++---------- 3 files changed, 206 insertions(+), 243 deletions(-) delete mode 100644 ltix/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php create mode 100644 ltix/service/gradebookservices/backup/moodle2/backup_ltixservice_gradebookservices_plugin.class.php rename ltix/service/gradebookservices/backup/moodle2/{restore_ltiservice_gradebookservices_subplugin.class.php => restore_ltixservice_gradebookservices_plugin.class.php} (50%) diff --git a/ltix/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php b/ltix/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php deleted file mode 100644 index bcda7cca898..00000000000 --- a/ltix/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php +++ /dev/null @@ -1,140 +0,0 @@ -. - -/** - * This file contains the class for restore of this gradebookservices plugin - * - * @package ltixservice_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 ltixservice_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_ltixservice_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('ltixservice_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 = \core_ltix\helper::get_tool_by_url_match($lti->toolurl, $activitycourseid); - if (!$tool) { - $tool = \core_ltix\helper::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 {ltixservice_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 {ltixservice_gradebookservices} l - WHERE l.courseid = ? - AND l.typeid = ? - AND l.toolproxyid is null"; - $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($typeid)]; - } - - $lineitem->set_source_sql($lineitemssql, $lineitemsparams); - - return $subplugin; - } -} diff --git a/ltix/service/gradebookservices/backup/moodle2/backup_ltixservice_gradebookservices_plugin.class.php b/ltix/service/gradebookservices/backup/moodle2/backup_ltixservice_gradebookservices_plugin.class.php new file mode 100644 index 00000000000..71189d0f418 --- /dev/null +++ b/ltix/service/gradebookservices/backup/moodle2/backup_ltixservice_gradebookservices_plugin.class.php @@ -0,0 +1,126 @@ +. + +/** + * Provides the information to backup gradebookservices lineitems + * + * @package ltixservice_gradebookservices + * @category backup + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_ltixservice_gradebookservices_plugin extends backup_plugin { + /** @var int TypeId contained in DB but is invalid */ + const NONVALIDTYPEID = 0; + + /** + * Returns the plugin information to attach to submission element + * + * @return backup_plugin_element + */ + protected function define_ltiresourcelink_plugin_structure(): backup_plugin_element { + global $DB; + + // Create XML elements. + $plugin = $this->get_plugin_element(); + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + // The gbs entries related to this element. + $lineitems = new backup_nested_element('lineitems'); + $lineitem = new backup_nested_element('lineitem', ['id'], [ + 'gradeitemid', + 'courseid', + 'toolproxyid', + 'typeid', + 'baseurl', + 'ltilinkid', + 'resourceid', + 'tag', + 'subreviewurl', + 'subreviewparams', + 'vendorcode', + 'guid', + ]); + + // Build the tree. + $plugin->add_child($pluginwrapper); + $pluginwrapper->add_child($lineitems); + $lineitems->add_child($lineitem); + + // Define sources. + $courseid = $this->task->get_courseid(); + $contextid = $this->task->get_contextid(); + $resourcelinks = $DB->get_records('lti_resource_link', ['contextid' => $contextid]); + $sourcearray = []; + foreach ($resourcelinks as $resourcelink) { + // We need to know the actual activity tool or toolproxy. + // If and activity is assigned to a type that doesn't exist, we don't want to backup any related lineitems.`` + // Default to invalid condition. + $typeid = 0; + $toolproxyid = '0'; + + $ltitype = $DB->get_record('lti_types', ['id' => $resourcelink->typeid], 'toolproxyid, baseurl'); + if ($ltitype) { + $typeid = $resourcelink->typeid; + $toolproxyid = $ltitype->toolproxyid; + } else if ($resourcelink->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. + // TODO: In the following code, I assumed that ltilinkid is repurposed to refer to `lti_resourcelink.id`. + $gbsrecord = $DB->get_record( + 'ltixservice_gradebookservices', + ['ltilinkid' => $resourcelink->id], + '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 = \core_ltix\helper::get_tool_by_url_match($resourcelink->url, $courseid); + 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; + } + } + } + } + + if ($toolproxyid != null) { + $lineitemssql = "SELECT l.*, t.vendorcode as vendorcode, t.guid as guid + FROM {ltixservice_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' => $courseid, $toolproxyid]; + } else { + $lineitemssql = "SELECT l.*, null as vendorcode, null as guid + FROM {ltixservice_gradebookservices} l + WHERE l.courseid = ? + AND l.typeid = ? + AND l.toolproxyid is null"; + $lineitemsparams = ['courseid' => $courseid, $typeid]; + } + $sourcearray += $DB->get_records_sql($lineitemssql, $lineitemsparams); + } + + $lineitem->set_source_array($sourcearray); + + return $plugin; + } +} diff --git a/ltix/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php b/ltix/service/gradebookservices/backup/moodle2/restore_ltixservice_gradebookservices_plugin.class.php similarity index 50% rename from ltix/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php rename to ltix/service/gradebookservices/backup/moodle2/restore_ltixservice_gradebookservices_plugin.class.php index 7fc54e05777..73ff773f115 100644 --- a/ltix/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php +++ b/ltix/service/gradebookservices/backup/moodle2/restore_ltixservice_gradebookservices_plugin.class.php @@ -12,44 +12,28 @@ // 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 . +// along with Moodle. If not, see . /** - * This file contains the class for restore of this gradebookservices plugin - * - * @package ltixservice_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. + * Restore plugin class. * * Provides the necessary information - * needed to restore the lineitems related with the lti activity (coupled), + * needed to restore the lineitems related to an ltix tool (coupled), * and all the uncoupled ones from the course. * * @package ltixservice_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 + * @category backup + * @copyright 2025 Shamim Rezaie + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin { - +class restore_ltixservice_gradebookservices_plugin extends restore_plugin { /** - * Returns the subplugin structure to attach to the XML element. + * Returns the plugin 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(); + protected function define_ltiresourcelink_plugin_structure(): array { + $paths = []; $elename = $this->get_namefor('lineitem'); $elepath = $this->get_pathfor('/lineitems/lineitem'); $paths[] = new restore_path_element($elename, $elepath); @@ -59,20 +43,18 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin /** * Processes one lineitem * - * @param mixed $data + * @param array $data The lineitem data to restore. * @return void */ - public function process_ltixservice_gradebookservices_lineitem($data) { + public function process_ltixservice_gradebookservices_lineitem(array $data): void { global $DB; - $data = (object)$data; + $data = (object) $data; // The coupled lineitems are restored as any other grade item // so we will only create the entry in the ltixservice_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(); + + $courseid = $this->task->get_courseid(); if ($data->typeid != null) { if ($ltitypeid = $this->get_mappingid('ltitype', $data->typeid)) { $newtypeid = $ltitypeid; @@ -92,33 +74,34 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin } else { $newtoolproxyid = null; } + // TODO: In the following code, I assumed that ltilinkid is repurposed to refer to `lti_resourcelink.id`. if ($data->ltilinkid != null) { - if ($data->ltilinkid != $this->get_old_parentid('lti')) { + if ($data->ltilinkid != $this->get_old_parentid('ltiresourcelink')) { // This is a linked item, but not for the current lti link, so skip it. return; } - $ltilinkid = $this->get_new_parentid('lti'); + $ltilinkid = $this->get_new_parentid('ltiresourcelink'); } else { $ltilinkid = null; } $resourceid = null; - if (property_exists( $data, 'resourceid' )) { + 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('ltixservice_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 ?? '' - )); + if ($this->get_mappingid('gbsgradeitemrestored', $data->id, 0) == 0) { + $newgbsid = $DB->insert_record('ltixservice_gradebookservices', (object) [ + '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); } @@ -129,17 +112,17 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin * we try to find the toolproxyid. * If none is found, then we set it to 0. * - * @param mixed $data - * @return integer $newtoolproxyid + * @param stdClass $data An object containing the `guid` and `vendorcode` used to locate the proxy ID. + * @return int The proxy ID if found, or 0 if no matching proxy ID exists. */ - private function find_proxy_id($data) { + private function find_proxy_id(stdClass $data): int { 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); + $dbtoolproxyjsonparams = ['guid' => $oldtoolproxyguid, 'vendorcode' => $oldtoolproxyvendor]; + $dbtoolproxy = $DB->get_field('lti_tool_proxies', 'id', $dbtoolproxyjsonparams); if ($dbtoolproxy) { $newtoolproxyid = $dbtoolproxy; } @@ -147,48 +130,64 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin } /** - * If the typeid is not in the mapping or it is 0, (it should be most of the times) + * If the typeid is not in the mapping or it is 0, (it should be most of the time) * 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 + * @param stdClass $data The data object containing type information, including 'typeid' and 'baseurl'. + * @param int $courseid The ID of the course to search within. + * @return int The new-found LTI type ID or 0 if no matching type is found. */ - private function find_typeid($data, $courseid) { + private function find_typeid(stdClass $data, int $courseid): int { 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); + $dbtypeidparameter = [ + '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); + $dbtypeidparameter = ['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); + $dbtypeidparameter = ['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); + $dbtypeidparameter = ['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; } @@ -199,14 +198,13 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin } /** - * We call the after_restore_lti to update the grade_items id's that we didn't know in the moment of creating + * We call the after_restore_ltiresourcelink 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() { + protected function after_restore_ltiresourcelink(): void { global $DB; - $activitytask = $this->task; - $courseid = $activitytask->get_courseid(); - $gbstoupdate = $DB->get_records('ltixservice_gradebookservices', array('gradeitemid' => 0, 'courseid' => $courseid)); + $courseid = $this->task->get_courseid(); + $gbstoupdate = $DB->get_records('ltixservice_gradebookservices', ['gradeitemid' => 0, 'courseid' => $courseid]); foreach ($gbstoupdate as $gbs) { $oldgradeitemid = $this->get_mappingid('gbsgradeitemoldid', $gbs->id, 0); $newgradeitemid = $this->get_mappingid('grade_item', $oldgradeitemid, 0); @@ -214,33 +212,12 @@ class restore_ltixservice_gradebookservices_subplugin extends restore_subplugin $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]); + $gbs->resourceid = $DB->get_field('grade_items', 'idnumber', ['id' => $newgradeitemid]); } $DB->update_record('ltixservice_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('ltixservice_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 = \core_ltix\helper::get_instance_type($instance)) { - $DB->insert_record('ltixservice_gradebookservices', (object) array( - 'gradeitemid' => $gi->id, - 'courseid' => $courseid, - 'toolproxyid' => $tool->toolproxyid, - 'ltilinkid' => $gi->iteminstance, - 'typeid' => $tool->id, - 'baseurl' => $tool->baseurl, - 'resourceid' => $gi->idnumber - )); - } - } - } - } + // TODO: This is incomplete, we should also check if the lti_resourcelink has a gradebookservices record. } - } -- 2.39.5 (Apple Git-154) From 24337f124f79bb3e80060411d859884edc0cfa38 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Tue, 21 Jan 2025 05:28:06 +1100 Subject: [PATCH 3/4] MDL-81864 mod_lti: Update backup/restore Also removed `preferheight` which was a leftover from 285f82504645c67dc21dcdecc57c380698e5fbe9 --- .../backup/moodle2/backup_lti_stepslib.php | 179 ++---------------- .../restore_lti_activity_task.class.php | 12 ++ 2 files changed, 23 insertions(+), 168 deletions(-) diff --git a/mod/lti/backup/moodle2/backup_lti_stepslib.php b/mod/lti/backup/moodle2/backup_lti_stepslib.php index ae211860f3d..1ecc6354892 100644 --- a/mod/lti/backup/moodle2/backup_lti_stepslib.php +++ b/mod/lti/backup/moodle2/backup_lti_stepslib.php @@ -64,7 +64,7 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step $userinfo = $this->get_setting_value('userinfo'); // Define each element separated. - $lti = new backup_nested_element('lti', array('id'), array( + $lti = new backup_nested_element('lti', ['id'], [ 'name', 'intro', 'introformat', @@ -73,193 +73,36 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step 'typeid', 'toolurl', 'securetoolurl', - 'preferheight', - 'launchcontainer', 'instructorchoicesendname', 'instructorchoicesendemailaddr', - 'instructorchoiceacceptgrades', 'instructorchoiceallowroster', 'instructorchoiceallowsetting', - 'grade', 'instructorcustomparameters', + 'instructorchoiceacceptgrades', + 'grade', + 'launchcontainer', + new encrypted_final_element('resourcekey'), + new encrypted_final_element('password'), 'debuglaunch', 'showtitlelaunch', 'showdescriptionlaunch', 'icon', 'secureicon', - new encrypted_final_element('resourcekey'), - new encrypted_final_element('password'), - ) - ); - - $ltitype = new backup_nested_element('ltitype', array('id'), array( - 'name', - 'baseurl', - 'tooldomain', - 'state', - 'course', - 'coursevisible', - 'ltiversion', - 'clientid', - 'toolproxyid', - 'enabledcapability', - 'parameter', - 'icon', - 'secureicon', - 'createdby', - 'timecreated', - 'timemodified', - 'description' - ) - ); - - $ltitypesconfigs = new backup_nested_element('ltitypesconfigs'); - $ltitypesconfig = new backup_nested_element('ltitypesconfig', array('id'), array( - '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', - ) - ); - - $ltisubmissions = new backup_nested_element('ltisubmissions'); - $ltisubmission = new backup_nested_element('ltisubmission', array('id'), array( - 'userid', - 'datesubmitted', - 'dateupdated', - 'gradepercent', - 'originalgrade', - 'launchid', - 'state' - )); - - $lticoursevisible = new backup_nested_element('lticoursevisible', ['id'], [ - 'typeid', - 'courseid', - 'coursevisible', ]); - // Build the tree - $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); - $lti->add_child($ltisubmissions); - $ltisubmissions->add_child($ltisubmission); - $lti->add_child($lticoursevisible); + // Build the tree. + // (No tree). // Define sources. - $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([]); - } + $lti->set_source_table('lti', ['id' => backup::VAR_ACTIVITYID]); - // All the rest of elements only happen if we are including user info. - if ($userinfo) { - $ltisubmission->set_source_table('lti_submission', array('ltiid' => backup::VAR_ACTIVITYID)); - } - - $lticoursevisibledata = $this->retrieve_lti_coursevisible($ltirecord); - $lticoursevisible->set_source_array($lticoursevisibledata ? [$lticoursevisibledata] : []); - - // Define id annotations - $ltitype->annotate_ids('user', 'createdby'); - $ltitype->annotate_ids('course', 'course'); - $ltisubmission->annotate_ids('user', 'userid'); + // Define id annotations. + // (none). // Define file annotations. $lti->annotate_files('mod_lti', 'intro', null); // This file areas haven't itemid. - // Add support for subplugin structures. - $this->add_subplugin_structure('ltisource', $lti, true); - $this->add_subplugin_structure('ltiservice', $lti, true); - // 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; - } - - /** - * Retrieves a record from {lti_coursevisible} table associated with the current type - * - * @param stdClass $ltirecord record from {lti} table - * @return mixed - */ - protected function retrieve_lti_coursevisible(stdClass $ltirecord): mixed { - global $DB; - if (!$ltirecord->typeid) { - return null; - } - return $DB->get_record('lti_coursevisible', ['typeid' => $ltirecord->typeid, 'courseid' => $ltirecord->course]); - } } diff --git a/mod/lti/backup/moodle2/restore_lti_activity_task.class.php b/mod/lti/backup/moodle2/restore_lti_activity_task.class.php index 1fa90aabe96..eccc9389ac4 100644 --- a/mod/lti/backup/moodle2/restore_lti_activity_task.class.php +++ b/mod/lti/backup/moodle2/restore_lti_activity_task.class.php @@ -129,4 +129,16 @@ class restore_lti_activity_task extends restore_activity_task { return $rules; } + + /** + * Returns the itemname for the ltiresourcelink mapping. + * + * @param string $itemtype The type of item, not used here. + * @return string The itemname for the ltiresourcelink mapping. + */ + public function get_ltiresourcelink_mapping_itemname($itemtype) { + // The itemname for the ltiresourcelink mapping is always 'course_module'. + return 'course_module'; + + } } -- 2.39.5 (Apple Git-154) From 4fec9793277fdf8c0ae2d34bab0029c39f71f29f Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Tue, 3 Jun 2025 10:53:53 +1000 Subject: [PATCH 4/4] MDL-81864 core_ltix: Handle placements in backup/restore --- backup/moodle2/backup_stepslib.php | 30 ++++++++++++++ backup/moodle2/restore_stepslib.php | 63 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 79fec1aa1cd..76353be04d5 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -3200,6 +3200,21 @@ class backup_ltixtypes_structure_step extends backup_structure_step { 'coursevisible', ]); + $ltiplacements = new backup_nested_element('ltiplacements'); + $ltiplacement = new backup_nested_element('ltiplacement', ['id'], [ + 'placementtype', + 'placementcomponent', + ]); + $ltiplacementconfigs = new backup_nested_element('ltiplacementconfigs'); + $ltiplacementconfig = new backup_nested_element('ltiplacementconfig', ['id'], [ + 'name', + 'value', + ]); + $ltiplacementstatus = new backup_nested_element('ltiplacementstatus', ['id'], [ + 'contextid', + 'status', + ]); + // Build the tree. $ltitypes->add_child($ltitype); $ltitype->add_child($ltitypesconfigs); @@ -3209,6 +3224,11 @@ class backup_ltixtypes_structure_step extends backup_structure_step { $ltitoolproxy->add_child($ltitoolsettings); $ltitoolsettings->add_child($ltitoolsetting); $ltitype->add_child($lticoursevisible); + $ltitype->add_child($ltiplacements); + $ltiplacements->add_child($ltiplacement); + $ltiplacement->add_child($ltiplacementconfigs); + $ltiplacementconfigs->add_child($ltiplacementconfig); + $ltiplacement->add_child($ltiplacementstatus); // Define sources. $ltitypearray = $this->retrieve_lti_types(); @@ -3239,6 +3259,16 @@ class backup_ltixtypes_structure_step extends backup_structure_step { $params ); + $ltiplacement->set_source_sql( + "SELECT lp.id, lpt.type AS placementtype, lpt.component AS placementcomponent + FROM {lti_placement} lp + JOIN {lti_placement_types} lpt ON lpt.id = lp.placementtypeid + WHERE lp.toolid = ?", + [backup::VAR_PARENTID] + ); + $ltiplacementconfig->set_source_table('lti_placement_config', ['placementid' => backup::VAR_PARENTID]); + $ltiplacementstatus->set_source_table('lti_placement_status', ['placementid' => backup::VAR_PARENTID]); + // If this is LTI 2 tool add settings for the current activity. $ltitoolproxy->set_source_sql( "SELECT toolproxyid AS id FROM {lti_types} WHERE id = ? AND toolproxyid IS NOT NULL", diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index aacbf32cb8b..5785d0c0472 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -6842,6 +6842,15 @@ class restore_ltixtypes_structure_step extends restore_structure_step { $paths[] = new restore_path_element('ltitoolproxy', '/ltitypes/ltitype/ltitoolproxy'); $paths[] = new restore_path_element('ltitoolsetting', '/ltitypes/ltitype/ltitoolproxy/ltitoolsettings/ltitoolsetting'); $paths[] = new restore_path_element('lticoursevisible', '/ltitypes/ltitype/lticoursevisible'); + $paths[] = new restore_path_element('ltiplacement', '/ltitypes/ltitype/ltiplacements/ltiplacement'); + $paths[] = new restore_path_element( + 'ltiplacementconfig', + '/ltitypes/ltitype/ltiplacements/ltiplacement/ltiplacementconfigs/ltiplacementconfig' + ); + $paths[] = new restore_path_element( + 'ltiplacementstatus', + '/ltitypes/ltitype/ltiplacements/ltiplacement/ltiplacementstatus' + ); // Add support for ltix plugin structures. $this->add_plugin_structure('ltixsource', $ltitype); @@ -6967,6 +6976,60 @@ class restore_ltixtypes_structure_step extends restore_structure_step { $DB->insert_record('lti_coursevisible', $data); } } + + /** + * Processes an LTI ltiplacement restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltiplacement(array $data): void { + global $DB; + + $data = (object) $data; + $oldid = $data->id; + + // TODO: Make a decision on how to handle unavailable placement types. + $data->placementtypeid = $DB->get_field('lti_placement_types', 'id', [ + 'type' => $data->placementtype, + 'component' => $data->placementcomponent, + ]); + if ($data->placementtypeid) { + $ltiplacementid = $DB->insert_record('lti_placements', $data); + $this->set_mapping('ltiplacement', $oldid, $ltiplacementid); + } + } + + /** + * Processes an LTI ltiplacementconfig restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltiplacementconfig(array $data): void { + global $DB; + + $data = (object) $data; + $data->placementid = $this->get_new_parentid('ltiplacement'); + + if ($data->placementid) { + $DB->insert_record('lti_placement_config', $data); + } + } + + /** + * Processes an LTI ltiplacementstatus restore + * + * @param array $data The data from the backup XML file + */ + protected function process_ltiplacementstatus(array $data): void { + global $DB; + + $data = (object) $data; + $data->placementid = $this->get_new_parentid('ltiplacement'); + + if ($data->placementid) { + $DB->insert_record('lti_placement_status', $data); + } + } } /** -- 2.39.5 (Apple Git-154)