diff --git a/admin/tool/messageinbound/classes/manager.php b/admin/tool/messageinbound/classes/manager.php index 74de88ac136..16f7d19f67f 100644 --- a/admin/tool/messageinbound/classes/manager.php +++ b/admin/tool/messageinbound/classes/manager.php @@ -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. * diff --git a/admin/tool/messageinbound/classes/privacy/provider.php b/admin/tool/messageinbound/classes/privacy/provider.php new file mode 100644 index 00000000000..de6fc9a0144 --- /dev/null +++ b/admin/tool/messageinbound/classes/privacy/provider.php @@ -0,0 +1,173 @@ +. + +/** + * Data provider. + * + * @package tool_messageinbound + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @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 + * @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; + } + +} diff --git a/admin/tool/messageinbound/classes/task/cleanup_task.php b/admin/tool/messageinbound/classes/task/cleanup_task.php index e8b1c7a1978..fc2c01d9093 100644 --- a/admin/tool/messageinbound/classes/task/cleanup_task.php +++ b/admin/tool/messageinbound/classes/task/cleanup_task.php @@ -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(); } } diff --git a/admin/tool/messageinbound/lang/en/tool_messageinbound.php b/admin/tool/messageinbound/lang/en/tool_messageinbound.php index 20c1c4774f0..cab10b571e3 100644 --- a/admin/tool/messageinbound/lang/en/tool_messageinbound.php +++ b/admin/tool/messageinbound/lang/en/tool_messageinbound.php @@ -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'; diff --git a/admin/tool/messageinbound/tests/manager_test.php b/admin/tool/messageinbound/tests/manager_test.php new file mode 100644 index 00000000000..936d99636a0 --- /dev/null +++ b/admin/tool/messageinbound/tests/manager_test.php @@ -0,0 +1,97 @@ +. + +/** + * Manager tests. + * + * @package tool_messageinbound + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @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 + * @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' => 'text@example.com', + 'timecreated' => time(), + ], $params); + $record->id = $DB->insert_record('messageinbound_messagelist', $record); + return $record; + } + +} diff --git a/admin/tool/messageinbound/tests/privacy_test.php b/admin/tool/messageinbound/tests/privacy_test.php new file mode 100644 index 00000000000..5be172c1f4d --- /dev/null +++ b/admin/tool/messageinbound/tests/privacy_test.php @@ -0,0 +1,201 @@ +. + +/** + * Data provider tests. + * + * @package tool_messageinbound + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @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 + * @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' => 'u1@example1.com']); + $this->create_messagelist(['userid' => $u1->id, 'address' => 'u1@example2.com']); + $this->create_messagelist(['userid' => $u2->id, 'address' => 'u2@example1.com']); + + // 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('u1@example1.com', $data->messages_pending_validation[0]['received_at']); + $this->assertEquals('u1@example2.com', $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' => 'text@example.com', + 'timecreated' => time(), + ], $params); + $record->id = $DB->insert_record('messageinbound_messagelist', $record); + return $record; + } + +}