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.
This commit is contained in:
@@ -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));
|
||||
$ltirecord = $DB->get_record('lti', ['id' => $this->task->get_activityid()]);
|
||||
$lti->set_source_array([$ltirecord]);
|
||||
|
||||
$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));
|
||||
$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));
|
||||
$oldid = $data->id;
|
||||
if (!empty($data->createdby)) {
|
||||
$data->createdby = $this->get_mappingid('user', $data->createdby) ?: $USER->id;
|
||||
}
|
||||
|
||||
// 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);
|
||||
$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
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@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
|
||||
@@ -1,35 +0,0 @@
|
||||
@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"
|
||||
Reference in New Issue
Block a user