Merge branch 'MDL-62214-33-56' of git://github.com/FMCorz/moodle into MOODLE_33_STABLE
This commit is contained in:
@@ -310,6 +310,16 @@ class manager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove older verification failures.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function tidy_old_verification_failures() {
|
||||
global $DB;
|
||||
$DB->delete_records_select('messageinbound_messagelist', 'timecreated < :time', ['time' => time() - DAYSECS]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a message and pass it through the Inbound Message handling systems.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_messageinbound\privacy;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use context;
|
||||
use context_user;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
/**
|
||||
* Data provider class.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns metadata.
|
||||
*
|
||||
* @param collection $collection The initialised collection to add items to.
|
||||
* @return collection A listing of user data stored through this system.
|
||||
*/
|
||||
public static function get_metadata(collection $collection) {
|
||||
|
||||
$collection->add_database_table('messageinbound_messagelist', [
|
||||
'messageid' => 'privacy:metadata:messagelist:messageid',
|
||||
'userid' => 'privacy:metadata:messagelist:userid',
|
||||
'address' => 'privacy:metadata:messagelist:address',
|
||||
'timecreated' => 'privacy:metadata:messagelist:timecreated',
|
||||
], 'privacy:metadata:messagelist');
|
||||
|
||||
// Arguably the keys are handled by \core\message\inbound\address_manager and thus could/should be handled by core.
|
||||
$collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:coreuserkey');
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
* @param int $userid The user to search.
|
||||
* @return \contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid($userid) {
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
|
||||
// Always add the user context so we're sure we're not dodging user keys, besides it's not costly to do so.
|
||||
$contextlist->add_user_context($userid);
|
||||
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||||
*/
|
||||
public static function export_user_data(approved_contextlist $contextlist) {
|
||||
global $DB;
|
||||
if (!static::approved_contextlist_contains_my_context($contextlist)) {
|
||||
// We only care about the user's user context.
|
||||
return;
|
||||
}
|
||||
|
||||
$userid = $contextlist->get_user()->id;
|
||||
$context = context_user::instance($userid);
|
||||
$path = [get_string('messageinbound', 'tool_messageinbound')];
|
||||
|
||||
// Export user keys.
|
||||
\core_userkey\privacy\provider::export_userkeys($context, $path, 'messageinbound_handler');
|
||||
|
||||
// Export the message list.
|
||||
$data = [];
|
||||
$recordset = $DB->get_recordset('messageinbound_messagelist', ['userid' => $userid], 'timecreated, id');
|
||||
foreach ($recordset as $record) {
|
||||
$data[] = [
|
||||
'received_at' => $record->address,
|
||||
'timecreated' => transform::datetime($record->timecreated),
|
||||
];
|
||||
}
|
||||
$recordset->close();
|
||||
writer::with_context($context)->export_data($path, (object) ['messages_pending_validation' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data for all users in the specified context.
|
||||
*
|
||||
* @param context $context The specific context to delete data for.
|
||||
*/
|
||||
public static function delete_data_for_all_users_in_context(context $context) {
|
||||
global $DB;
|
||||
if ($context->contextlevel != CONTEXT_USER) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::delete_user_data($context->instanceid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
global $DB;
|
||||
if (!static::approved_contextlist_contains_my_context($contextlist)) {
|
||||
// We only care about the user's user context.
|
||||
return;
|
||||
}
|
||||
|
||||
static::delete_user_data($contextlist->get_user()->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user's data.
|
||||
*
|
||||
* @param int $userid The user ID.
|
||||
* @return void
|
||||
*/
|
||||
protected static function delete_user_data($userid) {
|
||||
global $DB;
|
||||
$DB->delete_records_select('messageinbound_messagelist', 'userid = :userid', ['userid' => $userid]);
|
||||
\core_userkey\privacy\provider::delete_userkeys('messageinbound_handler', $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the contextlist contains our own context.
|
||||
*
|
||||
* @param approved_contextlist $contextlist The contextlist
|
||||
* @return bool
|
||||
*/
|
||||
protected static function approved_contextlist_contains_my_context(approved_contextlist $contextlist) {
|
||||
$userid = $contextlist->get_user()->id;
|
||||
foreach ($contextlist->get_contexts() as $context) {
|
||||
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,6 +49,7 @@ class cleanup_task extends \core\task\scheduled_task {
|
||||
*/
|
||||
public function execute() {
|
||||
$manager = new \tool_messageinbound\manager();
|
||||
return $manager->tidy_old_messages();
|
||||
$manager->tidy_old_messages();
|
||||
$manager->tidy_old_verification_failures();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,12 @@ $string['onehour'] = 'One hour';
|
||||
$string['oneweek'] = 'One week';
|
||||
$string['oneyear'] = 'One year';
|
||||
$string['pluginname'] = 'Inbound message configuration';
|
||||
$string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
|
||||
$string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
|
||||
$string['privacy:metadata:messagelist:address'] = 'The address at which the email was sent';
|
||||
$string['privacy:metadata:messagelist:messageid'] = 'The message ID';
|
||||
$string['privacy:metadata:messagelist:timecreated'] = 'The time at which the record was made';
|
||||
$string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
|
||||
$string['replysubjectprefix'] = 'Re:';
|
||||
$string['requirevalidation'] = 'Validate sender address';
|
||||
$string['name'] = 'Name';
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Manager tests.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @category test
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use tool_messageinbound\privacy\provider;
|
||||
|
||||
/**
|
||||
* Manager testcase class.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @category test
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_messageinbound_manager_testcase extends provider_testcase {
|
||||
|
||||
public function setUp() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Pretend the system is enabled.
|
||||
$CFG->messageinbound_enabled = true;
|
||||
$CFG->messageinbound_mailbox = 'mailbox';
|
||||
$CFG->messageinbound_domain = 'example.com';
|
||||
}
|
||||
|
||||
public function test_tidy_old_verification_failures() {
|
||||
global $DB;
|
||||
|
||||
$now = time();
|
||||
$stale = time() - DAYSECS;
|
||||
|
||||
$this->create_messagelist(['timecreated' => $now]);
|
||||
$this->create_messagelist(['timecreated' => $now - HOURSECS]);
|
||||
$this->create_messagelist(['timecreated' => $stale]);
|
||||
$this->create_messagelist(['timecreated' => $stale - HOURSECS]);
|
||||
$this->create_messagelist(['timecreated' => $stale - YEARSECS]);
|
||||
|
||||
$this->assertEquals(5, $DB->count_records('messageinbound_messagelist', []));
|
||||
$this->assertEquals(3, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
|
||||
|
||||
$manager = new \tool_messageinbound\manager();
|
||||
$manager->tidy_old_verification_failures();
|
||||
|
||||
$this->assertEquals(2, $DB->count_records('messageinbound_messagelist', []));
|
||||
$this->assertEquals(0, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message to validate.
|
||||
*
|
||||
* @param array $params The params.
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function create_messagelist(array $params) {
|
||||
global $DB, $USER;
|
||||
$record = (object) array_merge([
|
||||
'messageid' => 'abc',
|
||||
'userid' => $USER->id,
|
||||
'address' => '[email protected]',
|
||||
'timecreated' => time(),
|
||||
], $params);
|
||||
$record->id = $DB->insert_record('messageinbound_messagelist', $record);
|
||||
return $record;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Data provider tests.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @category test
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use tool_messageinbound\privacy\provider;
|
||||
|
||||
/**
|
||||
* Data provider testcase class.
|
||||
*
|
||||
* @package tool_messageinbound
|
||||
* @category test
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_messageinbound_privacy_testcase extends provider_testcase {
|
||||
|
||||
public function setUp() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Pretend the system is enabled.
|
||||
$CFG->messageinbound_enabled = true;
|
||||
$CFG->messageinbound_mailbox = 'mailbox';
|
||||
$CFG->messageinbound_domain = 'example.com';
|
||||
}
|
||||
|
||||
public function test_get_contexts_for_userid() {
|
||||
$dg = $this->getDataGenerator();
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u1ctx = context_user::instance($u1->id);
|
||||
$u2ctx = context_user::instance($u2->id);
|
||||
|
||||
$contexts = provider::get_contexts_for_userid($u1->id)->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
$this->assertEquals($u1ctx->id, $contexts[0]->id);
|
||||
|
||||
$contexts = provider::get_contexts_for_userid($u2->id)->get_contexts();
|
||||
$this->assertCount(1, $contexts);
|
||||
$this->assertEquals($u2ctx->id, $contexts[0]->id);
|
||||
}
|
||||
|
||||
public function test_delete_data_for_user() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u1ctx = context_user::instance($u1->id);
|
||||
$u2ctx = context_user::instance($u2->id);
|
||||
|
||||
$addressmanager = new \core\message\inbound\address_manager();
|
||||
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
|
||||
$addressmanager->set_data(123);
|
||||
|
||||
// Create a user key for both users.
|
||||
$addressmanager->generate($u1->id);
|
||||
$addressmanager->generate($u2->id);
|
||||
|
||||
// Create a messagelist for both users.
|
||||
$this->create_messagelist(['userid' => $u1->id]);
|
||||
$this->create_messagelist(['userid' => $u2->id]);
|
||||
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
|
||||
|
||||
// Passing another user's context does not do anything.
|
||||
provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u2ctx->id]));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
|
||||
|
||||
// Deleting user 1.
|
||||
provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id]));
|
||||
$this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
|
||||
}
|
||||
|
||||
public function test_delete_data_for_all_users_in_context() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u1ctx = context_user::instance($u1->id);
|
||||
$u2ctx = context_user::instance($u2->id);
|
||||
|
||||
$addressmanager = new \core\message\inbound\address_manager();
|
||||
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
|
||||
$addressmanager->set_data(123);
|
||||
|
||||
// Create a user key for both users.
|
||||
$addressmanager->generate($u1->id);
|
||||
$addressmanager->generate($u2->id);
|
||||
|
||||
// Create a messagelist for both users.
|
||||
$this->create_messagelist(['userid' => $u1->id]);
|
||||
$this->create_messagelist(['userid' => $u2->id]);
|
||||
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
|
||||
|
||||
// Deleting user 1.
|
||||
provider::delete_data_for_all_users_in_context($u1ctx);
|
||||
$this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
|
||||
$this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
|
||||
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
|
||||
}
|
||||
|
||||
public function test_export_data_for_user() {
|
||||
$dg = $this->getDataGenerator();
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u1ctx = context_user::instance($u1->id);
|
||||
$u2ctx = context_user::instance($u2->id);
|
||||
|
||||
$addressmanager = new \core\message\inbound\address_manager();
|
||||
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
|
||||
$addressmanager->set_data(123);
|
||||
|
||||
// Create a user key for both users.
|
||||
$addressmanager->generate($u1->id);
|
||||
$addressmanager->generate($u2->id);
|
||||
|
||||
// Create a messagelist for both users.
|
||||
$this->create_messagelist(['userid' => $u1->id, 'address' => '[email protected]']);
|
||||
$this->create_messagelist(['userid' => $u1->id, 'address' => '[email protected]']);
|
||||
$this->create_messagelist(['userid' => $u2->id, 'address' => '[email protected]']);
|
||||
|
||||
// Export for user.
|
||||
$this->setUser($u1);
|
||||
provider::export_user_data(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id, $u2ctx->id]));
|
||||
$data = writer::with_context($u2ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
|
||||
$this->assertEmpty($data);
|
||||
$data = writer::with_context($u1ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
|
||||
$this->assertCount(2, $data->messages_pending_validation);
|
||||
$this->assertEquals('[email protected]', $data->messages_pending_validation[0]['received_at']);
|
||||
$this->assertEquals('[email protected]', $data->messages_pending_validation[1]['received_at']);
|
||||
|
||||
$data = writer::with_context($u2ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
|
||||
$this->assertEmpty($data);
|
||||
$data = writer::with_context($u1ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
|
||||
$this->assertCount(1, $data->keys);
|
||||
$this->assertEquals('messageinbound_handler', $data->keys[0]->script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message to validate.
|
||||
*
|
||||
* @param array $params The params.
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function create_messagelist(array $params) {
|
||||
global $DB, $USER;
|
||||
$record = (object) array_merge([
|
||||
'messageid' => 'abc',
|
||||
'userid' => $USER->id,
|
||||
'address' => '[email protected]',
|
||||
'timecreated' => time(),
|
||||
], $params);
|
||||
$record->id = $DB->insert_record('messageinbound_messagelist', $record);
|
||||
return $record;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user