From 88f53cf84e1994d1568a593dc71bffccbbce772e Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Fri, 20 Apr 2018 14:54:12 +0800 Subject: [PATCH] MDL-61919 core_message: implement privacy provider --- lang/en/message.php | 24 + message/classes/privacy/provider.php | 409 ++++++++++++++++++ message/tests/privacy_provider_test.php | 553 ++++++++++++++++++++++++ 3 files changed, 986 insertions(+) create mode 100644 message/classes/privacy/provider.php create mode 100644 message/tests/privacy_provider_test.php diff --git a/lang/en/message.php b/lang/en/message.php index 64a7588c064..e653399b097 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -95,6 +95,29 @@ $string['outputenabled'] = 'Output enabled'; $string['outputnotavailable'] = 'Not available'; $string['outputnotconfigured'] = 'Not configured'; $string['permitted'] = 'Permitted'; +$string['privacy:metadata:messages'] = 'Message/notifications'; +$string['privacy:metadata:messages:component'] = 'The component responsible for sending the message/notification'; +$string['privacy:metadata:messages:contexturl'] = 'The URL related to a notification'; +$string['privacy:metadata:messages:contexturlname'] = 'The name of the context'; +$string['privacy:metadata:messages:eventtype'] = 'The event type'; +$string['privacy:metadata:messages:fullmessage'] = 'The full message/notification'; +$string['privacy:metadata:messages:fullmessageformat'] = 'The message/notification format'; +$string['privacy:metadata:messages:fullmessagehtml'] = 'The HTML of the message/notification'; +$string['privacy:metadata:messages:notification'] = 'Flag whether or not this is a notification'; +$string['privacy:metadata:messages:smallmessage'] = 'The small message/notification'; +$string['privacy:metadata:messages:subject'] = 'The subject of the message/notification'; +$string['privacy:metadata:messages:timecreated'] = 'The date at which the message/notification was created'; +$string['privacy:metadata:messages:timeread'] = 'The date at which the message/notification was read'; +$string['privacy:metadata:messages:timeuserfromdeleted'] = 'The date at which the message/notification was deleted by the sender'; +$string['privacy:metadata:messages:timeusertodeleted'] = 'The date at which the message/notification was deleted by the receiver'; +$string['privacy:metadata:messages:useridfrom'] = 'The ID of the user who sent the notification'; +$string['privacy:metadata:messages:useridto'] = 'The ID of the user who received the notification'; +$string['privacy:metadata:message_contacts'] = 'The list of contacts'; +$string['privacy:metadata:message_contacts:blocked'] = 'Flag whether or not the user is blocked'; +$string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact'; +$string['privacy:metadata:message_contacts:userid'] = 'The ID of the user whose contact list we are viewing'; +$string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging'; +$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\''; $string['processorsettings'] = 'Processor settings'; $string['removecontact'] = 'Remove contact'; $string['removecoursefilter'] = 'Remove filter for course {$a}'; @@ -122,6 +145,7 @@ $string['togglenotificationmenu'] = 'Toggle notifications menu'; $string['togglemessagemenu'] = 'Toggle messages menu'; $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) that doesn\'t exist'; $string['unblockcontact'] = 'Unblock contact'; +$string['unknownuser'] = 'Unknown user'; $string['unreadnotification'] = 'Unread notification: {$a}'; $string['unreadnewmessage'] = 'New message from {$a}'; $string['userisblockingyou'] = 'This user has blocked you from sending messages to them'; diff --git a/message/classes/privacy/provider.php b/message/classes/privacy/provider.php new file mode 100644 index 00000000000..e0e27d2988d --- /dev/null +++ b/message/classes/privacy/provider.php @@ -0,0 +1,409 @@ +. + +/** + * Privacy Subsystem implementation for core_message. + * + * @package core_message + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_message\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for core_message. + * + * @copyright 2018 Mark Nelson + * @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\subsystem\provider, + \core_privacy\local\request\user_preference_provider { + + /** + * Return the fields which contain personal data. + * + * @param collection $items a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $items) { + $items->add_database_table( + 'message', + [ + 'useridfrom' => 'privacy:metadata:messages:useridfrom', + 'useridto' => 'privacy:metadata:messages:useridto', + 'subject' => 'privacy:metadata:messages:subject', + 'fullmessage' => 'privacy:metadata:messages:fullmessage', + 'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat', + 'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml', + 'smallmessage' => 'privacy:metadata:messages:smallmessage', + 'notification' => 'privacy:metadata:messages:notification', + 'contexturl' => 'privacy:metadata:messages:contexturl', + 'contexturlname' => 'privacy:metadata:messages:contexturlname', + 'component' => 'privacy:metadata:messages:component', + 'eventtype' => 'privacy:metadata:messages:eventtype', + 'timecreated' => 'privacy:metadata:messages:timecreated', + 'timeuserfromdeleted' => 'privacy:metadata:messages:timeuserfromdeleted', + 'timeusertodeleted' => 'privacy:metadata:messages:timeusertodeleted' + ], + 'privacy:metadata:messages' + ); + + $items->add_database_table( + 'message_read', + [ + 'useridfrom' => 'privacy:metadata:messages:useridfrom', + 'useridto' => 'privacy:metadata:messages:useridto', + 'subject' => 'privacy:metadata:messages:subject', + 'fullmessage' => 'privacy:metadata:messages:fullmessage', + 'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat', + 'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml', + 'smallmessage' => 'privacy:metadata:messages:smallmessage', + 'notification' => 'privacy:metadata:messages:notification', + 'contexturl' => 'privacy:metadata:messages:contexturl', + 'contexturlname' => 'privacy:metadata:messages:contexturlname', + 'component' => 'privacy:metadata:messages:component', + 'eventtype' => 'privacy:metadata:messages:eventtype', + 'timecreated' => 'privacy:metadata:messages:timecreated', + 'timeread' => 'privacy:metadata:messages:timeread', + 'timeuserfromdeleted' => 'privacy:metadata:messages:timeuserfromdeleted', + 'timeusertodeleted' => 'privacy:metadata:messages:timeusertodeleted' + ], + 'privacy:metadata:messages' + ); + + $items->add_database_table( + 'message_contacts', + [ + 'userid' => 'privacy:metadata:message_contacts:userid', + 'contactid' => 'privacy:metadata:message_contacts:contactid', + 'blocked' => 'privacy:metadata:message_contacts:blocked', + ], + 'privacy:metadata:message_contacts' + ); + + // Now add that we also have user preferences. + $items->add_user_preference('core_message_messageprovider_settings', + 'privacy:metadata:preference:core_message_settings'); + + return $items; + } + + /** + * Store all user preferences for core message. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences($userid) { + $preferences = get_user_preferences(null, null, $userid); + foreach ($preferences as $name => $value) { + if ((substr($name, 0, 16) == 'message_provider') || ($name == 'message_blocknoncontacts')) { + writer::export_user_preference( + 'core_message', + $name, + $value, + get_string('privacy:request:preference:set', 'core_message', (object) [ + 'name' => $name, + 'value' => $value, + ]) + ); + } + } + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid($userid) { + // Messages are in the system context. + $contextlist = new contextlist(); + $contextlist->add_system_context(); + + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + if (empty($contextlist->count())) { + return; + } + + // Remove non-system contexts. If it ends up empty then early return. + $contexts = array_filter($contextlist->get_contexts(), function($context) { + return $context->contextlevel == CONTEXT_SYSTEM; + }); + + if (empty($contexts)) { + return; + } + + $userid = $contextlist->get_user()->id; + + // Export the contacts. + self::export_user_data_contacts($userid); + + // Export the notifications. + self::export_user_data_notifications($userid); + + // Export the messages, with any related actions. + self::export_user_data_messages($userid); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (!$context instanceof \context_system) { + return; + } + + $DB->delete_records('message'); + $DB->delete_records('message_read'); + $DB->delete_records('message_contacts'); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + // Remove non-system contexts. If it ends up empty then early return. + $contexts = array_filter($contextlist->get_contexts(), function($context) { + return $context->contextlevel == CONTEXT_SYSTEM; + }); + + if (empty($contexts)) { + return; + } + + $userid = $contextlist->get_user()->id; + + $DB->delete_records_select('message', 'useridfrom = ? AND notification = 0', [$userid]); + $DB->delete_records_select('message_read', 'useridfrom = ? AND notification = 0', [$userid]); + $DB->delete_records_select('message', '(useridfrom = ? OR useridto = ?) AND notification = 1', [$userid, $userid]); + $DB->delete_records_select('message_read', '(useridfrom = ? OR useridto = ?) AND notification = 1', [$userid, $userid]); + $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]); + } + + /** + * Export the messaging contact data. + * + * @param int $userid + */ + protected static function export_user_data_contacts($userid) { + global $DB; + + $context = \context_system::instance(); + + // Get the user's contacts. + if ($contacts = $DB->get_records('message_contacts', ['userid' => $userid], 'id ASC')) { + $contactdata = []; + foreach ($contacts as $contact) { + $contactdata[] = (object) [ + 'contact' => transform::user($contact->contactid), + 'blocked' => transform::yesno($contact->blocked) + ]; + } + writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata); + } + } + + /** + * Export the messaging data. + * + * @param int $userid + */ + protected static function export_user_data_messages($userid) { + global $DB; + + $context = \context_system::instance(); + + $users = self::get_userids_in_conversation_with($userid); + if (!empty($users)) { + // Ok, let's get the other users details. Note - the user may no longer exist. + $userids = array_keys($users); + list($useridsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $userfields = \user_picture::fields('u'); + $userssql = "SELECT $userfields + FROM {user} u + WHERE u.id $useridsql + AND u.deleted = 0"; + $otherusers = $DB->get_records_sql($userssql, $userparams + ['userid' => $userid]); + foreach ($users as $user) { + $otheruserfullname = get_string('unknownuser', 'core_message'); + + // It's possible the other user may have been deleted. + if (isset($otherusers[$user->id])) { + $otheruserfullname = fullname($otherusers[$user->id]); + } + + $sql = "SELECT id, useridfrom, useridto, subject, fullmessage, fullmessageformat, + fullmessagehtml, smallmessage, notification, contexturl, + contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted, + component, eventtype, 0 as timeread + FROM {message} m + WHERE notification = 0 + AND (useridfrom = ? AND useridto = ?) OR (useridfrom = ? AND useridto = ?) + UNION ALL + SELECT id, useridfrom, useridto, subject, fullmessage, fullmessageformat, + fullmessagehtml, smallmessage, notification, contexturl, + contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted, + component, eventtype, timeread + FROM {message_read} mr + WHERE notification = 0 + AND (useridfrom = ? AND useridto = ?) OR (useridfrom = ? AND useridto = ?) + ORDER BY timecreated ASC"; + $params = [$userid, $user->id, $user->id, $userid, $userid, $user->id, $user->id, $userid]; + $messages = $DB->get_recordset_sql($sql, $params); + $messagedata = []; + foreach ($messages as $message) { + $timeread = !empty($message->timeread) ? transform::datetime($message->timeread) : '-'; + $issender = $userid == $message->useridfrom; + + $timedeletedfield = 'timeusertodeleted'; + if ($issender) { + $timedeletedfield = 'timeuserfromdeleted'; + } + + $data = [ + 'sender' => transform::yesno($issender), + 'message' => message_format_message_text($message), + 'timecreated' => transform::datetime($message->timecreated), + 'timeread' => $timeread + ]; + + if (!empty($message->$timedeletedfield)) { + $data['timedeleted'] = transform::datetime($message->$timedeletedfield); + } + + $messagedata[] = (object) $data; + } + $messages->close(); + + writer::with_context($context)->export_data([get_string('messages', 'core_message'), $otheruserfullname], + (object) $messagedata); + } + } + } + + /** + * Export the notification data. + * + * @param int $userid + */ + protected static function export_user_data_notifications($userid) { + global $DB; + + $context = \context_system::instance(); + + $notificationdata = []; + $sql = "SELECT id, useridfrom, useridto, subject, fullmessage, fullmessageformat, + fullmessagehtml, smallmessage, notification, contexturl, + contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted, + component, eventtype, 0 as timeread + FROM {message} m + WHERE notification = 1 AND useridfrom = ? OR useridto = ? + UNION ALL + SELECT id, useridfrom, useridto, subject, fullmessage, fullmessageformat, + fullmessagehtml, smallmessage, notification, contexturl, + contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted, + component, eventtype, timeread + FROM {message_read} mr + WHERE notification = 1 AND useridfrom = ? OR useridto = ? + ORDER BY timecreated DESC"; + $notifications = $DB->get_recordset_sql($sql, [$userid, $userid, $userid, $userid]); + foreach ($notifications as $notification) { + $timeread = !empty($notification->timeread) ? transform::datetime($notification->timeread) : '-'; + + $data = (object) [ + 'subject' => $notification->subject, + 'fullmessage' => $notification->fullmessage, + 'smallmessage' => $notification->smallmessage, + 'component' => $notification->component, + 'eventtype' => $notification->eventtype, + 'contexturl' => $notification->contexturl, + 'contexturlname' => $notification->contexturlname, + 'timeread' => $timeread, + 'timecreated' => transform::datetime($notification->timecreated) + ]; + + $notificationdata[] = $data; + } + $notifications->close(); + + writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata); + } + + /** + * Returns a list of users the given user is in a conversation with. + * + * @param int $userid + * @return array + * @throws \dml_exception + */ + private static function get_userids_in_conversation_with($userid) { + global $DB; + + $sql = "SELECT DISTINCT(useridfrom) as id + FROM {message} m + WHERE useridto = ? + AND notification = 0 + UNION + SELECT DISTINCT(useridfrom) as id + FROM {message_read} m + WHERE useridto = ? + AND notification = 0"; + $users = $DB->get_records_sql($sql, [$userid, $userid]); + + $sql = "SELECT DISTINCT(useridto) as id + FROM {message} m + WHERE useridfrom = ? + AND notification = 0 + UNION + SELECT DISTINCT(useridto) as id + FROM {message_read} m + WHERE useridfrom = ? + AND notification = 0"; + return $users + $DB->get_records_sql($sql, [$userid, $userid]); + } +} diff --git a/message/tests/privacy_provider_test.php b/message/tests/privacy_provider_test.php new file mode 100644 index 00000000000..12f7b167e3a --- /dev/null +++ b/message/tests/privacy_provider_test.php @@ -0,0 +1,553 @@ +. + +/** + * Privacy provider tests. + * + * @package core_message + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_message\privacy\provider; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\transform; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package core_message + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('core_message'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(4, $itemcollection); + + $messagetable = array_shift($itemcollection); + $this->assertEquals('message', $messagetable->get_name()); + + $messagereadtable = array_shift($itemcollection); + $this->assertEquals('message_read', $messagereadtable->get_name()); + + $messagecontacts = array_shift($itemcollection); + $this->assertEquals('message_contacts', $messagecontacts->get_name()); + + $usersettings = array_shift($itemcollection); + $this->assertEquals('core_message_messageprovider_settings', $usersettings->get_name()); + + $privacyfields = $messagetable->get_privacy_fields(); + $this->assertArrayHasKey('useridfrom', $privacyfields); + $this->assertArrayHasKey('useridto', $privacyfields); + $this->assertArrayHasKey('subject', $privacyfields); + $this->assertArrayHasKey('fullmessage', $privacyfields); + $this->assertArrayHasKey('fullmessageformat', $privacyfields); + $this->assertArrayHasKey('fullmessagehtml', $privacyfields); + $this->assertArrayHasKey('smallmessage', $privacyfields); + $this->assertArrayHasKey('component', $privacyfields); + $this->assertArrayHasKey('eventtype', $privacyfields); + $this->assertArrayHasKey('contexturl', $privacyfields); + $this->assertArrayHasKey('contexturlname', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertEquals('privacy:metadata:messages', $messagetable->get_summary()); + + $privacyfields = $messagereadtable->get_privacy_fields(); + $this->assertArrayHasKey('useridfrom', $privacyfields); + $this->assertArrayHasKey('useridto', $privacyfields); + $this->assertArrayHasKey('subject', $privacyfields); + $this->assertArrayHasKey('fullmessage', $privacyfields); + $this->assertArrayHasKey('fullmessageformat', $privacyfields); + $this->assertArrayHasKey('fullmessagehtml', $privacyfields); + $this->assertArrayHasKey('smallmessage', $privacyfields); + $this->assertArrayHasKey('component', $privacyfields); + $this->assertArrayHasKey('eventtype', $privacyfields); + $this->assertArrayHasKey('contexturl', $privacyfields); + $this->assertArrayHasKey('contexturlname', $privacyfields); + $this->assertArrayHasKey('timeread', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertEquals('privacy:metadata:messages', $messagereadtable->get_summary()); + + $privacyfields = $messagecontacts->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('contactid', $privacyfields); + $this->assertArrayHasKey('blocked', $privacyfields); + $this->assertEquals('privacy:metadata:message_contacts', $messagecontacts->get_summary()); + } + + /** + * Test for provider::export_user_preferences(). + */ + public function test_export_user_preferences_no_pref() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + provider::export_user_preferences($user->id); + + $writer = writer::with_context(\context_system::instance()); + + $this->assertFalse($writer->has_any_data()); + } + + /** + * Test for provider::export_user_preferences(). + */ + public function test_export_user_preferences() { + global $USER; + + $this->resetAfterTest(); + + $this->setAdminUser(); + + // Create another user to set a preference for who we won't be exporting. + $user = $this->getDataGenerator()->create_user(); + + // Set some message user preferences. + set_user_preference('message_provider_moodle_instantmessage_loggedin', 'airnotifier', $USER->id); + set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'popup', $USER->id); + set_user_preference('message_blocknoncontacts', 1, $USER->id); + set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'inbound', $user->id); + + // Set an unrelated preference. + set_user_preference('block_myoverview_last_tab', 'courses', $USER->id); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + + $this->assertTrue($writer->has_any_data()); + + $prefs = (array) $writer->get_user_preferences('core_message'); + + // Check only 3 preferences exist. + $this->assertCount(3, $prefs); + $this->assertArrayHasKey('message_provider_moodle_instantmessage_loggedin', $prefs); + $this->assertArrayHasKey('message_provider_moodle_instantmessage_loggedoff', $prefs); + $this->assertArrayHasKey('message_blocknoncontacts', $prefs); + + foreach ($prefs as $key => $pref) { + if ($key == 'message_provider_moodle_instantmessage_loggedin') { + $this->assertEquals('airnotifier', $pref->value); + } else if ($key == 'message_provider_moodle_instantmessage_loggedoff') { + $this->assertEquals('popup', $pref->value); + } else { + $this->assertEquals(1, $pref->value); + } + } + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + $contextforuser = $contextlist->current(); + $this->assertEquals(SYSCONTEXTID, $contextforuser->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_with_contacts() { + $this->resetAfterTest(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + // This user will not be added as a contact. + $this->getDataGenerator()->create_user(); + + message_add_contact($user2->id, 0, $user1->id); + message_add_contact($user3->id, 0, $user1->id); + message_add_contact($user4->id, 1, $user1->id); + + $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message'); + + $writer = writer::with_context(\context_system::instance()); + + $contacts = (array) $writer->get_data([get_string('contacts', 'core_message')]); + + $this->assertCount(3, $contacts); + + $contact1 = array_shift($contacts); + $this->assertEquals($user2->id, $contact1->contact); + $this->assertEquals(get_string('no'), $contact1->blocked); + + $contact2 = array_shift($contacts); + $this->assertEquals($user3->id, $contact2->contact); + $this->assertEquals(get_string('no'), $contact2->blocked); + + $contact3 = array_shift($contacts); + $this->assertEquals($user4->id, $contact3->contact); + $this->assertEquals(get_string('yes'), $contact3->blocked); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_with_messages() { + global $DB; + + $this->resetAfterTest(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + $now = time(); + + // Send messages from user 1 to user 2. + $m1 = $this->create_message_or_notification($user1->id, $user2->id, $now - (9 * DAYSECS), false, $now); + $m2 = $this->create_message_or_notification($user2->id, $user1->id, $now - (8 * DAYSECS)); + $m3 = $this->create_message_or_notification($user1->id, $user2->id, $now - (7 * DAYSECS)); + + // Send messages from user 3 to user 1. + $m4 = $this->create_message_or_notification($user3->id, $user1->id, $now - (6 * DAYSECS), false, $now); + $m5 = $this->create_message_or_notification($user1->id, $user3->id, $now - (5 * DAYSECS)); + $m6 = $this->create_message_or_notification($user3->id, $user1->id, $now - (4 * DAYSECS)); + + // Send messages from user 3 to user 2 - these should not be included in the export. + $m7 = $this->create_message_or_notification($user3->id, $user2->id, $now - (3 * DAYSECS), false, $now); + $m8 = $this->create_message_or_notification($user2->id, $user3->id, $now - (2 * DAYSECS)); + $m9 = $this->create_message_or_notification($user3->id, $user2->id, $now - (1 * DAYSECS)); + + // Mark message 2 and 5 as deleted. + $dbm2 = $DB->get_record('message', ['id' => $m2]); + $dbm5 = $DB->get_record('message', ['id' => $m5]); + message_delete_message($dbm2, $user1->id); + message_delete_message($dbm5, $user1->id); + + $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message'); + + $writer = writer::with_context(\context_system::instance()); + + $this->assertTrue($writer->has_any_data()); + + // Confirm the messages with user 2 are correct. + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), fullname($user2)]); + $this->assertCount(3, $messages); + + $dbm1 = $DB->get_record('message_read', ['id' => $m1]); + $dbm2 = $DB->get_record('message', ['id' => $m2]); + $dbm3 = $DB->get_record('message', ['id' => $m3]); + + $m1 = array_shift($messages); + $m2 = array_shift($messages); + $m3 = array_shift($messages); + + $this->assertEquals(get_string('yes'), $m1->sender); + $this->assertEquals(message_format_message_text($dbm1), $m1->message); + $this->assertEquals(transform::datetime($now - (9 * DAYSECS)), $m1->timecreated); + $this->assertNotEquals('-', $m1->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m1); + + $this->assertEquals(get_string('no'), $m2->sender); + $this->assertEquals(message_format_message_text($dbm2), $m2->message); + $this->assertEquals(transform::datetime($now - (8 * DAYSECS)), $m2->timecreated); + $this->assertEquals('-', $m2->timeread); + $this->assertArrayHasKey('timedeleted', (array) $m2); + + $this->assertEquals(get_string('yes'), $m3->sender); + $this->assertEquals(message_format_message_text($dbm3), $m3->message); + $this->assertEquals(transform::datetime($now - (7 * DAYSECS)), $m3->timecreated); + $this->assertEquals('-', $m3->timeread); + + // Confirm the messages with user 3 are correct. + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), fullname($user3)]); + $this->assertCount(3, $messages); + + $dbm4 = $DB->get_record('message_read', ['id' => $m4]); + $dbm5 = $DB->get_record('message', ['id' => $m5]); + $dbm6 = $DB->get_record('message', ['id' => $m6]); + + $m4 = array_shift($messages); + $m5 = array_shift($messages); + $m6 = array_shift($messages); + + $this->assertEquals(get_string('no'), $m4->sender); + $this->assertEquals(message_format_message_text($dbm4), $m4->message); + $this->assertEquals(transform::datetime($now - (6 * DAYSECS)), $m4->timecreated); + $this->assertNotEquals('-', $m4->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m4); + + $this->assertEquals(get_string('yes'), $m5->sender); + $this->assertEquals(message_format_message_text($dbm5), $m5->message); + $this->assertEquals(transform::datetime($now - (5 * DAYSECS)), $m5->timecreated); + $this->assertEquals('-', $m5->timeread); + $this->assertArrayHasKey('timedeleted', (array) $m5); + + $this->assertEquals(get_string('no'), $m6->sender); + $this->assertEquals(message_format_message_text($dbm6), $m6->message); + $this->assertEquals(transform::datetime($now - (4 * DAYSECS)), $m6->timecreated); + $this->assertEquals('-', $m6->timeread); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_with_notifications() { + $this->resetAfterTest(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + $now = time(); + $timeread = $now - DAYSECS; + + // Send notifications from user 1 to user 2. + $this->create_message_or_notification($user1->id, $user2->id, $now + (9 * DAYSECS), true, $timeread); + $this->create_message_or_notification($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $this->create_message_or_notification($user1->id, $user2->id, $now + (7 * DAYSECS), true); + + // Send notifications from user 3 to user 1. + $this->create_message_or_notification($user3->id, $user1->id, $now + (6 * DAYSECS), true, $timeread); + $this->create_message_or_notification($user1->id, $user3->id, $now + (5 * DAYSECS), true); + $this->create_message_or_notification($user3->id, $user1->id, $now + (4 * DAYSECS), true); + + // Send notifications from user 3 to user 2 - should not be part of the export. + $this->create_message_or_notification($user3->id, $user2->id, $now + (3 * DAYSECS), true, $timeread); + $this->create_message_or_notification($user2->id, $user3->id, $now + (2 * DAYSECS), true); + $this->create_message_or_notification($user3->id, $user2->id, $now + (1 * DAYSECS), true); + + $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message'); + + $writer = writer::with_context(\context_system::instance()); + + $this->assertTrue($writer->has_any_data()); + + // Confirm the notifications. + $notifications = (array) $writer->get_data([get_string('notifications', 'core_message')]); + + $this->assertCount(6, $notifications); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $now = time(); + $timeread = $now - DAYSECS; + + $systemcontext = \context_system::instance(); + + // Create contacts. + message_add_contact($user1->id, 0, $user2->id); + message_add_contact($user2->id, 0, $user1->id); + + // Create messages. + $m1 = $this->create_message_or_notification($user1->id, $user2->id, $now + (9 * DAYSECS), false, $timeread); + $m2 = $this->create_message_or_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $m3 = $this->create_message_or_notification($user2->id, $user1->id, $now + (7 * DAYSECS), false, $timeread); + $m4 = $this->create_message_or_notification($user1->id, $user2->id, $now + (6 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_message_or_notification($user1->id, $user2->id, $now + (9 * DAYSECS), true, $timeread); + $n2 = $this->create_message_or_notification($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $m3 = $this->create_message_or_notification($user2->id, $user1->id, $now + (7 * DAYSECS), true, $timeread); + $m4 = $this->create_message_or_notification($user1->id, $user2->id, $now + (6 * DAYSECS), true); + + // Delete one of the messages. + $dbm2 = $DB->get_record('message', ['id' => $m2]); + message_delete_message($dbm2, $user1->id); + + // There should be two contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be two unread messages. + $this->assertEquals(2, $DB->count_records('message', ['notification' => 0])); + + // There should be two read messages. + $this->assertEquals(2, $DB->count_records('message_read', ['notification' => 0])); + + // There should be two unread notifications. + $this->assertEquals(2, $DB->count_records('message', ['notification' => 1])); + + // There should be two read notifications. + $this->assertEquals(2, $DB->count_records('message_read', ['notification' => 1])); + + provider::delete_data_for_all_users_in_context($systemcontext); + + // Confirm all has been deleted. + $this->assertEquals(0, $DB->count_records('message_contacts')); + $this->assertEquals(0, $DB->count_records('message')); + $this->assertEquals(0, $DB->count_records('message_read')); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + $now = time(); + $timeread = $now - DAYSECS; + + // Create contacts. + message_add_contact($user1->id, 0, $user2->id); + message_add_contact($user2->id, 0, $user1->id); + message_add_contact($user2->id, 0, $user3->id); + + // Create messages. + $m1 = $this->create_message_or_notification($user1->id, $user2->id, $now + (9 * DAYSECS), false, $timeread); + $m2 = $this->create_message_or_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $m3 = $this->create_message_or_notification($user2->id, $user1->id, $now + (7 * DAYSECS), false, $timeread); + $m4 = $this->create_message_or_notification($user1->id, $user2->id, $now + (6 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_message_or_notification($user1->id, $user2->id, $now + (9 * DAYSECS), true, $timeread); + $n2 = $this->create_message_or_notification($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $n3 = $this->create_message_or_notification($user2->id, $user3->id, $now + (8 * DAYSECS), true); + $n4 = $this->create_message_or_notification($user3->id, $user2->id, $now + (8 * DAYSECS), true, $timeread); + + // Delete one of the messages. + $dbm2 = $DB->get_record('message', ['id' => $m2]); + message_delete_message($dbm2, $user1->id); + + // There should be three contacts. + $this->assertEquals(3, $DB->count_records('message_contacts')); + + // There should be two unread messages. + $this->assertEquals(2, $DB->count_records('message', ['notification' => 0])); + + // There should be two read messages. + $this->assertEquals(2, $DB->count_records('message_read', ['notification' => 0])); + + // There should be two unread notifications. + $this->assertEquals(2, $DB->count_records('message', ['notification' => 1])); + + // There should be two read notifications. + $this->assertEquals(2, $DB->count_records('message_read', ['notification' => 1])); + + $systemcontext = \context_system::instance(); + $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_message', + [$systemcontext->id]); + provider::delete_data_for_user($contextlist); + + // Confirm the user 2 data still exists. + $contacts = $DB->get_records('message_contacts'); + $messages = $DB->get_records('message', ['notification' => 0]); + $messagesread = $DB->get_records('message_read', ['notification' => 0]); + $notifications = $DB->get_records('message', ['notification' => 1]); + $notificationsread = $DB->get_records('message_read', ['notification' => 1]); + + $this->assertCount(1, $contacts); + $contact = reset($contacts); + $this->assertEquals($user3->id, $contact->userid); + $this->assertEquals($user2->id, $contact->contactid); + + $this->assertCount(1, $messages); + $message = reset($messages); + $this->assertEquals($m2, $message->id); + + $this->assertCount(1, $messagesread); + $messagesread = reset($messagesread); + $this->assertEquals($m3, $messagesread->id); + + $this->assertCount(1, $notifications); + $notifications = reset($notifications); + $this->assertEquals($n3, $notifications->id); + + $this->assertCount(1, $notificationsread); + $notificationsread = reset($notificationsread); + $this->assertEquals($n4, $notificationsread->id); + } + + /** + * Creates a message or notification to be used for testing. + * + * @param int $useridfrom The user id from + * @param int $useridto The user id to + * @param int $timecreated + * @param bool $notification + * @param int|null $timeread The time the message/notification was read, null if it hasn't been. + * @return int The id of the message (in either the message or message_read table) + * @throws dml_exception + */ + private function create_message_or_notification($useridfrom, $useridto, $timecreated = null, + $notification = false, $timeread = null) { + global $DB; + + $tabledata = new \stdClass(); + + if (is_null($timecreated)) { + $timecreated = time(); + } + + if (!is_null($timeread)) { + $table = 'message_read'; + $tabledata->timeread = $timeread; + } else { + $table = 'message'; + } + + if ($notification) { + $tabledata->eventtype = 'assign_notification'; + $tabledata->component = 'mod_assign'; + $tabledata->notification = 1; + $tabledata->contexturl = 'https://www.google.com'; + $tabledata->contexturlname = 'google'; + } else { + $tabledata->eventtype = 'instantmessage'; + $tabledata->component = 'moodle'; + $tabledata->notification = 0; + } + + $tabledata->useridfrom = $useridfrom; + $tabledata->useridto = $useridto; + $tabledata->subject = 'Subject ' . $timecreated; + $tabledata->fullmessage = 'Full message ' . $timecreated; + $tabledata->fullmessageformat = FORMAT_PLAIN; + $tabledata->fullmessagehtml = 'Full message HTML ' . $timecreated; + $tabledata->smallmessage = 'Small message ' . $timecreated; + $tabledata->timecreated = $timecreated; + + return $DB->insert_record($table, $tabledata); + } +}