From 5de4b14160026a7b72d505b3cb7eeb3eea2ba883 Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Tue, 27 Feb 2018 15:17:25 +0800 Subject: [PATCH] MDL-61307 core_privacy: Add the privacy manager --- privacy/classes/local/request/helper.php | 2 +- privacy/classes/manager.php | 348 ++++++++++++++++++ .../mock_mod_with_user_data_provider.php | 54 +++ privacy/tests/fixtures/mock_null_provider.php | 42 +++ .../mock_plugin_subplugin_provider.php | 47 +++ privacy/tests/fixtures/mock_provider.php | 86 +++++ privacy/tests/manager_test.php | 193 ++++++++++ 7 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 privacy/classes/manager.php create mode 100644 privacy/tests/fixtures/mock_mod_with_user_data_provider.php create mode 100644 privacy/tests/fixtures/mock_null_provider.php create mode 100644 privacy/tests/fixtures/mock_plugin_subplugin_provider.php create mode 100644 privacy/tests/fixtures/mock_provider.php create mode 100644 privacy/tests/manager_test.php diff --git a/privacy/classes/local/request/helper.php b/privacy/classes/local/request/helper.php index fcc627eaea8..a593cf6f882 100644 --- a/privacy/classes/local/request/helper.php +++ b/privacy/classes/local/request/helper.php @@ -77,7 +77,7 @@ class helper { $data = static::get_context_data($context, $user); static::export_context_files($context, $user); - writer::with_context($context)->write_data([], $data); + writer::with_context($context)->export_data([], $data); } } diff --git a/privacy/classes/manager.php b/privacy/classes/manager.php new file mode 100644 index 00000000000..973c95568dd --- /dev/null +++ b/privacy/classes/manager.php @@ -0,0 +1,348 @@ +. + +/** + * This file contains the core_privacy\manager class. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\contextlist_collection; +use core_privacy\local\request\deletion_criteria; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The core_privacy\manager class, providing a facade to describe, export and delete personal data across Moodle and its components. + * + * This class is responsible for communicating with and collating privacy data from all relevant components, where relevance is + * determined through implementations of specific marker interfaces. These marker interfaces describe the responsibilities (in terms + * of personal data storage) as well as the relationship between the component and the core_privacy subsystem. + * + * The interface hierarchy is as follows: + * ├── local\metadata\null_provider + * ├── local\metadata\provider + * ├── local\request\data_provider + * └── local\request\core_data_provider + * └── local\request\core_user_data_provider + * └── local\request\plugin\provider + * └── local\request\subsystem\provider + * └── local\request\user_preference_provider + * └── local\request\shared_data_provider + * └── local\request\plugin\subsystem_provider + * └── local\request\plugin\subplugin_provider + * └── local\request\subsystem\plugin_provider + * + * Describing personal data: + * ------------------------- + * All components must state whether they store personal data (and DESCRIBE it) by implementing one of the metadata providers: + * - local\metadata\null_provider (indicating they don't store personal data) + * - local\metadata\provider (indicating they do store personal data, and describing it) + * + * The manager requests metadata for all Moodle components implementing the local\metadata\provider interface. + * + * Export and deletion of personal data: + * ------------------------------------- + * Those components storing personal data need to provide EXPORT and DELETION of this data by implementing a request provider. + * Which provider implementation depends on the nature of the component; whether it's a sub-component and which components it + * stores data for. + * + * Export and deletion for sub-components (or any component storing data on behalf of another component) is managed by the parent + * component. If a component contains sub-components, it must ask those sub-components to provide the relevant data. Only certain + * 'core provider' components are called directly from the manager and these must provide the personal data stored by both + * themselves, and by all sub-components. Because of this hierarchical structure, the core_privacy\manager needs to know which + * components are to be called directly by core: these are called core data providers. The providers implemented by sub-components + * are called shared data providers. + * + * The following are interfaces are not implemented directly, but are marker interfaces uses to classify components by nature: + * - local\request\data_provider: + * Not implemented directly. Used to classify components storing personal data of some kind. Includes both components storing + * personal data for themselves and on behalf of other components. + * Include: local\request\core_data_provider and local\request\shared_data_provider. + * - local\request\core_data_provider: + * Not implemented directly. Used to classify components storing personal data for themselves and which are to be called by the + * core_privacy subsystem directly. + * Includes: local\request\core_user_data_provider and local\request\user_preference_provider. + * - local\request\core_user_data_provider: + * Not implemented directly. Used to classify components storing personal data for themselves, which are either a plugin or + * subsystem and which are to be called by the core_privacy subsystem directly. + * Includes: local\request\plugin\provider and local\request\subsystem\provider. + * - local\request\shared_data_provider: + * Not implemented directly. Used to classify components storing personal data on behalf of other components and which are + * called by the owning component directly. + * Includes: local\request\plugin\subsystem_provider, local\request\plugin\subplugin_provider and local\request\subsystem\plugin_provider + * + * The manager only requests the export or deletion of personal data for components implementing the local\request\core_data_provider + * interface or one of its descendants; local\request\plugin\provider, local\request\subsystem\provider or local\request\user_preference_provider. + * Implementing one of these signals to the core_privacy subsystem that the component must be queried directly from the manager. + * + * Any component using another component to store personal data on its behalf, is responsible for making the relevant call to + * that component's relevant shared_data_provider class. + * + * For example: + * The manager calls a core_data_provider component (e.g. mod_assign) which, in turn, calls relevant subplugins or subsystems + * (which assign uses to store personal data) to get that data. All data for assign and its sub-components is aggregated by assign + * and returned to the core_privacy subsystem. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + /** + * Checks whether the given component is compliant with the core_privacy API. + * To be considered compliant, a component must declare whether (and where) it stores personal data. + * + * Components which do store personal data must: + * - Have implemented the core_privacy\local\metadata\provider interface (to describe the data it stores) and; + * - Have implemented the core_privacy\local\request\data_provider interface (to facilitate export of personal data) + * - Have implemented the core_privacy\local\request\deleter interface + * + * Components which do not store personal data must: + * - Have implemented the core_privacy\local\metadata\null_provider interface to signal that they don't store personal data. + * + * @param string $component frankenstyle component name, e.g. 'mod_assign' + * @return bool true if the component is compliant, false otherwise. + */ + public function component_is_compliant($component) { + // Components which don't store user data need only implement the null_provider. + if ($this->component_implements($component, \core_privacy\local\metadata\null_provider::class)) { + return true; + } + // Components which store user data must implement the local\metadata\provider and the local\request\data_provider. + if ($this->component_implements($component, \core_privacy\local\metadata\provider::class) && + $this->component_implements($component, \core_privacy\local\request\data_provider::class)) { + return true; + } + return false; + } + + /** + * Get the privacy metadata for all components. + * + * @return collection[] The array of collection objects, indexed by frankenstyle component name. + */ + public function get_metadata_for_components() { + // Get the metadata, and put into an assoc array indexed by component name. + $metadata = []; + foreach ($this->get_component_list() as $component) { + if ($this->component_implements($component, \core_privacy\local\metadata\provider::class)) { + $classname = $this->get_provider_classname($component); + $metadata[$component] = $classname::get_metadata(new collection($component)); + } + } + return $metadata; + } + + /** + * Gets a collection of resultset objects for all components. + * + * @param int $userid the id of the user we're fetching contexts for. + * @return contextlist_collection the collection of contextlist items for the respective components. + */ + public function get_contexts_for_userid($userid) { + $clcollection = new contextlist_collection($userid); + foreach ($this->get_component_list() as $component) { + if ($this->component_implements($component, \core_privacy\local\request\core_user_data_provider::class)) { + $classname = $this->get_provider_classname($component); + $contextlist = $classname::get_contexts_for_userid($userid); + } else { + $contextlist = new local\request\contextlist(); + } + + // Each contextlist is tied to its respective component. + $contextlist->set_component($component); + + // Add contexts that the component may not know about. + // Example of these include activity completion which modules do not know about themselves. + $contextlist = local\request\helper::add_shared_contexts_to_contextlist_for($userid, $contextlist); + + if (count($contextlist)) { + $clcollection->add_contextlist($contextlist); + } + } + + return $clcollection; + } + + /** + * Export all user data for the specified approved_contextlist items. + * + * Note: userid and component are stored in each respective approved_contextlist. + * + * @param contextlist_collection $contextlistcollection the collection of contextlists for all components. + * @return string the location of the exported data. + * @throws \moodle_exception if the contextlist_collection does not contain all approved_contextlist items or if one of the + * approved_contextlists' components is not a core_data_provider. + */ + public function export_user_data(contextlist_collection $contextlistcollection) { + // Export for the various components/contexts. + foreach ($contextlistcollection as $approvedcontextlist) { + if (!$approvedcontextlist instanceof \core_privacy\local\request\approved_contextlist) { + throw new \moodle_exception('Contextlist must be an approved_contextlist'); + } + + $component = $approvedcontextlist->get_component(); + // Core user data providers. + if ($this->component_implements($component, \core_privacy\local\request\core_user_data_provider::class)) { + if (count($approvedcontextlist)) { + // This plugin has data it knows about. It is responsible for storing basic data about anything it is + // told to export. + $classname = $this->get_provider_classname($component); + $classname::export_user_data($approvedcontextlist); + } + } else { + // This plugin does not know that it has data - export the shared data it doesn't know about. + local\request\helper::export_data_for_null_provider($approvedcontextlist); + } + } + + // Check each component for non contextlist items too. + foreach ($this->get_component_list() as $component) { + // Core user preference providers. + if ($this->component_implements($component, \core_privacy\local\request\user_preference_provider::class)) { + $classname = $this->get_provider_classname($component); + $classname::export_user_preferences($contextlistcollection->get_userid()); + } + } + + return local\request\writer::with_context(\context_system::instance())->finalise_content(); + } + + /** + * Delete all user data for approved contexts lists provided in the collection. + * + * This call relates to the forgetting of an entire user. + * + * Note: userid and component are stored in each respective approved_contextlist. + * + * @param contextlist_collection $contextlistcollection the collections of approved_contextlist items on which to call deletion. + * @throws \moodle_exception if the contextlist_collection doesn't contain all approved_contextlist items, or if the component + * for an approved_contextlist isn't a core provider. + */ + public function delete_user_data(contextlist_collection $contextlistcollection) { + // Delete the data. + foreach ($contextlistcollection as $approvedcontextlist) { + if (!$approvedcontextlist instanceof \core_privacy\local\request\approved_contextlist) { + throw new \moodle_exception('Contextlist must be an approved_contextlist'); + } + + if ($this->component_is_core_provider($approvedcontextlist->get_component())) { + if (count($approvedcontextlist)) { + // The component knows about data that it has. + // Have it delete its own data. + $classname = $this->get_provider_classname($approvedcontextlist->get_component()); + $classname::delete_user_data($approvedcontextlist); + } + } + + // Delete any shared user data it doesn't know about. + local\request\helper::delete_user_data($approvedcontextlist); + } + } + + /** + * Delete user data for all users using the specified deletion_criteria. + * + * @param deletion_criteria $criteria the criteria object dictating what contexts will be deleted. + */ + public function delete_for_context(deletion_criteria $criteria) { + foreach ($this->get_component_list() as $component) { + if ($this->component_implements($component, \core_privacy\local\request\core_user_data_provider::class)) { + // This component knows about specific data that it owns. + // Have it delete all of that user data for the context. + $classname = $this->get_provider_classname($component); + $classname::delete_for_context($criteria); + } + + // Delete any shared user data it doesn't know about. + local\request\helper::delete_for_context($component, $criteria); + } + } + + /** + * Check whether the specified component is a core provider. + * + * @param string $component the frankenstyle component name. + * @return bool true if the component is a core provider, false otherwise. + */ + protected function component_is_core_provider($component) { + return $this->component_implements($component, \core_privacy\local\request\core_data_provider::class); + } + + /** + * Returns a list of frankenstyle names of core components (plugins and subsystems). + * + * @return array the array of frankenstyle component names. + */ + protected function get_component_list() { + $components = []; + // Get all plugins. + $plugintypes = \core_component::get_plugin_types(); + foreach ($plugintypes as $plugintype => $typedir) { + $plugins = \core_component::get_plugin_list($plugintype); + foreach ($plugins as $pluginname => $plugindir) { + $components[] = $plugintype . '_' . $pluginname; + } + } + // Get all subsystems. + foreach (\core_component::get_core_subsystems() as $name => $path) { + if (isset($path)) { + $components[] = 'core_' . $name; + } + } + return $components; + } + + /** + * Return the fully qualified provider classname for the component. + * + * @param string $component the frankenstyle component name. + * @return string the fully qualified provider classname. + */ + protected function get_provider_classname($component) { + return static::get_provider_classname_for_component($component); + } + + /** + * Return the fully qualified provider classname for the component. + * + * @param string $component the frankenstyle component name. + * @return string the fully qualified provider classname. + */ + public static function get_provider_classname_for_component($component) { + return "$component\privacy\provider"; + } + + /** + * Checks whether the component's provider class implements the specified interface. + * This can either be implemented directly, or by implementing a descendant (extension) of the specified interface. + * + * @param string $component the frankenstyle component name. + * @param string $interface the name of the interface we want to check. + * @return bool True if an implementation was found, false otherwise. + */ + protected function component_implements($component, $interface) { + $providerclass = $this->get_provider_classname($component); + if (class_exists($providerclass)) { + $rc = new \ReflectionClass($providerclass); + return $rc->implementsInterface($interface); + } + return false; + } +} diff --git a/privacy/tests/fixtures/mock_mod_with_user_data_provider.php b/privacy/tests/fixtures/mock_mod_with_user_data_provider.php new file mode 100644 index 00000000000..5242ee58364 --- /dev/null +++ b/privacy/tests/fixtures/mock_mod_with_user_data_provider.php @@ -0,0 +1,54 @@ +. + +/** + * Test provider using a fake plugin name. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_testcomponent4\privacy; + +use core_privacy\local\request\writer; + +/** + * Mock core_user_data_provider for unit tests. + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, \core_privacy\local\request\user_preference_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'notimplemented'; + } + + /** + * Export all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences($userid) { + writer::export_user_preference('mod_testcomponent4', 'mykey', 'myvalue', 'mydescription'); + } +} diff --git a/privacy/tests/fixtures/mock_null_provider.php b/privacy/tests/fixtures/mock_null_provider.php new file mode 100644 index 00000000000..ef2fa3ad86c --- /dev/null +++ b/privacy/tests/fixtures/mock_null_provider.php @@ -0,0 +1,42 @@ +. + +/** + * Test null provider using a fake plugin name. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_testcomponent2\privacy; + +/** + * Mock null_provider for unit tests. + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string the reason for being a null provider. + */ + public static function get_reason() { + return 'testcomponent2 null provider reason'; + } +} diff --git a/privacy/tests/fixtures/mock_plugin_subplugin_provider.php b/privacy/tests/fixtures/mock_plugin_subplugin_provider.php new file mode 100644 index 00000000000..eb6ce76d11c --- /dev/null +++ b/privacy/tests/fixtures/mock_plugin_subplugin_provider.php @@ -0,0 +1,47 @@ +. + +/** + * Test provider using a fake plugin name. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_testcomponent3\privacy; + +use core_privacy\local\metadata\collection; + +/** + * Mock shared_data_provider for unit tests. + * @copyright 2018 Jake Dallimore + * @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\subplugin_provider { + + /** + * Returns meta data about this system. + * + * @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 = new collection('testcomponent3'); + $collection->add_database_table('testtable', ['testfield1', 'testfield2'], 'testsummary'); + return $collection; + } +} diff --git a/privacy/tests/fixtures/mock_provider.php b/privacy/tests/fixtures/mock_provider.php new file mode 100644 index 00000000000..ce52dfbb81b --- /dev/null +++ b/privacy/tests/fixtures/mock_provider.php @@ -0,0 +1,86 @@ +. + +/** + * Test provider using a fake plugin name. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_testcomponent\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\deletion_criteria; + +/** + * Mock core_user_data_provider for unit tests. + * @copyright 2018 Jake Dallimore + * @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 { + /** + * @return array The array of metadata. + */ + public static function get_metadata(collection $collection) { + $collection = new collection('testcomponent'); + $collection->add_database_table('testtable', ['testfield1', 'testfield2'], 'testsummary'); + 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) { + $cl = new contextlist(); + $cl->add_from_sql("SELECT c.id FROM {context} c WHERE c.id = :id", ['id' => \context_system::instance()->id]); + + return $cl; + } + + /** + * 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) { + // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. + } + + /** + * Delete all use data which matches the specified deletion_criteria. + * + * @param deletion_criteria $criteria An object containing specific deletion criteria to delete for. + */ + public static function delete_for_context(deletion_criteria $criteria) { + // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. + } + + /** + * 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_user_data(approved_contextlist $contextlist) { + // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. + } +} diff --git a/privacy/tests/manager_test.php b/privacy/tests/manager_test.php new file mode 100644 index 00000000000..980dfc3e581 --- /dev/null +++ b/privacy/tests/manager_test.php @@ -0,0 +1,193 @@ +. + +/** + * Privacy manager unit tests. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_null_provider.php'); +require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_provider.php'); +require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_plugin_subplugin_provider.php'); +require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_mod_with_user_data_provider.php'); + +use \core_privacy\local\request\writer; + +/** + * Privacy manager unit tests. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class privacy_manager_testcase extends advanced_testcase { + /** + * Test tearDown. + */ + public function tearDown() { + \core_privacy\local\request\writer::reset(); + } + + /** + * Helper to spoof the results of the internal function get_components_list, allowing mock components to be tested. + * + * @param array $componentnames and array of component names to include as valid core components. + * @return PHPUnit_Framework_MockObject_MockObject + */ + protected function get_mock_manager_with_core_components($componentnames) { + $mock = $this->getMockBuilder(\core_privacy\manager::class) + ->setMethods(['get_component_list']) + ->getMock(); + $mock->expects($this->any()) + ->method('get_component_list') + ->will($this->returnValue($componentnames)); + return $mock; + } + + /** + * Test collection of metadata for components implementing a metadata provider. + */ + public function test_get_metadata_for_components() { + // Get a mock manager, in which the core components list is mocked to include all mock plugins. + // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core). + $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']); + + // Core providers and shared providers both implement the metadata provider. + $collectionarray = $mockman->get_metadata_for_components(); + $this->assertArrayHasKey('mod_testcomponent', $collectionarray); + $collection = $collectionarray['mod_testcomponent']; + $this->assertInstanceOf(\core_privacy\local\metadata\collection::class, $collection); + $this->assertArrayHasKey('mod_testcomponent3', $collectionarray); + $collection = $collectionarray['mod_testcomponent3']; + $this->assertInstanceOf(\core_privacy\local\metadata\collection::class, $collection); + + // Component which implements just the local\metadata\null_provider. Metadata is not provided. + $this->assertArrayNotHasKey('mod_testcomponent2', $collectionarray); + } + + /** + * Test that get_contexts_for_userid() only returns contextlist collections for core providers. + */ + public function test_get_contexts_for_userid() { + // Get a mock manager, in which the core components list is mocked to include all mock plugins. + // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core). + $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']); + + // Get the contextlist_collection. + $contextlistcollection = $mockman->get_contexts_for_userid(10); + $this->assertInstanceOf(\core_privacy\local\request\contextlist_collection::class, $contextlistcollection); + + ob_flush(); + + // Verify we have a contextlist for the component in the collection. + $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, + $contextlistcollection->get_contextlist_for_component('mod_testcomponent')); + + // Verify we don't have a contextlist for the shared provider in the collection. + $this->assertNull($contextlistcollection->get_contextlist_for_component('mod_testcomponent3')); + + // Verify we don't have a contextlist for the component which does not store user data. + $this->assertEmpty($contextlistcollection->get_contextlist_for_component('mod_testcomponent2')); + } + + /** + * Test verifying the output of component_is_compliant. + */ + public function test_component_is_compliant() { + // Get a mock manager, in which the core components list is mocked to include all mock plugins. + // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core). + $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']); + + // A core_provider plugin implementing all required interfaces (local\metadata\provider, local\request\plugin_provider). + $this->assertTrue($mockman->component_is_compliant('mod_testcomponent')); + + // A component implementing just the \core_privacy\local\metadata\null_provider is compliant. + $this->assertTrue($mockman->component_is_compliant('mod_testcomponent2')); + + // A shared provider plugin implementing all required interfaces (local\metadata\provider, local\request\plugin\subplugin_provider) + // is compliant. + $this->assertTrue($mockman->component_is_compliant('mod_testcomponent3')); + + // A component implementing none of the providers. + $this->assertFalse($mockman->component_is_compliant('tool_thisisnotarealtool123')); + } + + /** + * Test verifying only approved contextlists can be used with the export_user_data method. + */ + public function test_export_user_data() { + // Get a mock manager, in which the core components list is mocked to include all mock plugins. + // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core). + $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3', 'mod_testcomponent4']); + + // Get the non-approved contextlists. + $contextlistcollection = $mockman->get_contexts_for_userid(10); + + // Create an approved contextlist. + $approvedcontextlistcollection = new \core_privacy\local\request\contextlist_collection(10); + foreach ($contextlistcollection->get_contextlists() as $contextlist) { + $approvedcontextlist = new \core_privacy\local\request\approved_contextlist(new stdClass(), $contextlist->get_component(), + $contextlist->get_contextids()); + $approvedcontextlistcollection->add_contextlist($approvedcontextlist); + } + // Verify the mocked return from the writer, meaning the manager method exited normally. + $this->assertEquals('mock_path', $mockman->export_user_data($approvedcontextlistcollection)); + + // Verify that a user preference was exported for 'mod_testcomponent4'. + $prefs = writer::with_context(\context_system::instance())->get_user_preferences('mod_testcomponent4'); + $this->assertNotEmpty($prefs); + $this->assertNotEmpty($prefs->mykey); + $this->assertEquals('myvalue', $prefs->mykey->value); + $this->assertEquals('mydescription', $prefs->mykey->description); + + // Verify an exception is thrown if trying to pass in a collection of non-approved_contextlist items. + $this->expectException(moodle_exception::class); + $mockman->export_user_data($contextlistcollection); + } + + /** + * Test verifying only approved contextlists can be used with the delete_user_data method. + */ + public function test_delete_user_data() { + $this->resetAfterTest(); + // Get a mock manager, in which the core components list is mocked to include all mock plugins. + // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core). + $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']); + + // Get the non-approved contextlists. + $user = \core_user::get_user_by_username('admin'); + $contextlistcollection = $mockman->get_contexts_for_userid($user->id); + + // Create an approved contextlist. + $approvedcontextlistcollection = new \core_privacy\local\request\contextlist_collection($user->id); + foreach ($contextlistcollection->get_contextlists() as $contextlist) { + $approvedcontextlist = new \core_privacy\local\request\approved_contextlist($user, $contextlist->get_component(), + $contextlist->get_contextids()); + $approvedcontextlistcollection->add_contextlist($approvedcontextlist); + } + + // Verify null, as the method has no return type and exits normally. Mainly checking we don't see any exception. + $this->assertNull($mockman->delete_user_data($approvedcontextlistcollection)); + + // Verify an exception is thrown if trying to pass in a collection of non-approved_contextlist items. + $this->expectException(moodle_exception::class); + $mockman->delete_user_data($contextlistcollection); + } +}