diff --git a/lang/en/portfolio.php b/lang/en/portfolio.php index 7e18f50f7f50c..e2c0810334bbd 100644 --- a/lang/en/portfolio.php +++ b/lang/en/portfolio.php @@ -167,6 +167,12 @@ $string['portfolio'] = 'Portfolio'; $string['portfolios'] = 'Portfolios'; $string['privacy:metadata'] = 'The portfolio subsystem acts as a channel, passing requests from plugins to the various portfolio plugins.'; +$string['privacy:metadata:name'] = 'Name of the preference.'; +$string['privacy:metadata:instance'] = 'Identifier for the portfolio.'; +$string['privacy:metadata:instancesummary'] = 'This stores portfolio both instances and preferences for the portfolios user is using.'; +$string['privacy:metadata:value'] = 'Value for the preference'; +$string['privacy:metadata:userid'] = 'The user Identifier.'; +$string['privacy:path'] = 'Portfolio instances'; $string['queuesummary'] = 'Currently queued transfers'; $string['returntowhereyouwere'] = 'Return to where you were'; $string['save'] = 'Save'; @@ -184,3 +190,4 @@ $string['wanttowait_high'] = 'It is not recommended that you wait for this transfer to complete, but you can if you\'re sure and know what you\'re doing'; $string['wanttowait_moderate'] = 'Do you want to wait for this transfer? It might take a few minutes'; + diff --git a/portfolio/classes/privacy/provider.php b/portfolio/classes/privacy/provider.php index f9686d4bf0575..3f1b7951a4a66 100644 --- a/portfolio/classes/privacy/provider.php +++ b/portfolio/classes/privacy/provider.php @@ -27,6 +27,8 @@ use core_privacy\local\metadata\collection; use core_privacy\local\request\context; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\approved_contextlist; /** * Provider for the portfolio API. @@ -35,10 +37,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements - // The Portfolio subsystem does not store any data itself. - // It has no database tables, and it purely acts as a conduit to the various portfolio plugins. + // The core portfolio system stores preferences related to the other portfolio subsystems. \core_privacy\local\metadata\provider, - + \core_privacy\local\request\plugin\provider, // The portfolio subsystem will be called by other components. \core_privacy\local\request\subsystem\plugin_provider { @@ -49,7 +50,100 @@ class provider implements * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { - return $collection->add_plugintype_link('portfolio', [], 'privacy:metadata'); + $collection->add_database_table('portfolio_instance_user', [ + 'instance' => 'privacy:metadata:instance', + 'userid' => 'privacy:metadata:userid', + 'name' => 'privacy:metadata:name', + 'value' => 'privacy:metadata:value' + ], 'privacy:metadata:instancesummary'); + $collection->add_plugintype_link('portfolio', [], 'privacy:metadata'); + 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(int $userid) : contextlist { + $sql = "SELECT ctx.id + FROM {context} ctx + JOIN {portfolio_instance_user} piu ON ctx.instanceid = piu.userid AND ctx.contextlevel = :usercontext + WHERE piu.userid = :userid"; + $params = ['userid' => $userid, 'usercontext' => CONTEXT_USER]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + 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 ($contextlist->get_component() != 'core_portfolio') { + return; + } + + $correctusercontext = array_filter($contextlist->get_contexts(), function($context) use ($contextlist) { + if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) { + return $context; + } + }); + + $usercontext = array_shift($correctusercontext); + + + $sql = "SELECT pi.name, piu.name AS preference, piu.value + FROM {portfolio_instance_user} piu + JOIN {portfolio_instance} pi ON piu.instance = pi.id + WHERE piu.userid = :userid"; + $params = ['userid' => $usercontext->instanceid]; + $instances = $DB->get_records_sql($sql, $params); + if (!empty($instances)) { + \core_privacy\local\request\writer::with_context($contextlist->current())->export_data( + [get_string('privacy:path', 'portfolio')], (object) $instances); + } + } + + /** + * 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; + // Context could be anything, BEWARE! + if ($context->contextlevel == CONTEXT_USER) { + $DB->delete_records('portfolio_instance_user', ['userid' => $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 ($contextlist->get_component() != 'core_portfolio') { + return; + } + + $correctusercontext = array_filter($contextlist->get_contexts(), function($context) use ($contextlist) { + if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) { + return $context; + } + }); + + $usercontext = array_shift($correctusercontext); + + $DB->delete_records('portfolio_instance_user', ['userid' => $usercontext->instanceid]); } /** diff --git a/portfolio/tests/privacy_provider_test.php b/portfolio/tests/privacy_provider_test.php index bc237966246d8..93ffb98167cbe 100644 --- a/portfolio/tests/privacy_provider_test.php +++ b/portfolio/tests/privacy_provider_test.php @@ -32,15 +32,110 @@ */ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testcase { + protected function create_portfolio_data($plugin, $name, $user, $preference, $value) { + global $DB; + $portfolioinstance = (object) [ + 'plugin' => $plugin, + 'name' => $name, + 'visible' => 1 + ]; + $portfolioinstance->id = $DB->insert_record('portfolio_instance', $portfolioinstance); + $userinstance = (object) [ + 'instance' => $portfolioinstance->id, + 'userid' => $user->id, + 'name' => $preference, + 'value' => $value + ]; + $DB->insert_record('portfolio_instance_user', $userinstance); + } + /** - * Verify that a collection of metadata is returned for this component and that it just links to the plugintype 'portfolio'. + * Verify that a collection of metadata is returned for this component and that it just returns the righ types for 'portfolio'. */ public function test_get_metadata() { $collection = new \core_privacy\local\metadata\collection('core_portfolio'); $collection = \core_portfolio\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); $items = $collection->get_collection(); - $this->assertEquals(1, count($items)); - $this->assertInstanceOf(\core_privacy\local\metadata\types\plugintype_link::class, $items[0]); + $this->assertEquals(2, count($items)); + $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[0]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\plugintype_link::class, $items[1]); + } + + /** + * Test that the export for a user id returns a user context. + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + $this->create_portfolio_data('googledocs', 'Google Docs', $user, 'visible', 1); + $contextlist = \core_portfolio\privacy\provider::get_contexts_for_userid($user->id); + $this->assertEquals($context->id, $contextlist->current()->id); + } + + /** + * Test that exporting user data works as expected. + */ + public function test_export_user_data() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $context = context_user::instance($user->id); + $this->create_portfolio_data('googledocs', 'Google Docs', $user, 'visible', 1); + $contextlist = new \core_privacy\local\request\approved_contextlist($user, 'core_portfolio', [$context->id]); + \core_portfolio\privacy\provider::export_user_data($contextlist); + $writer = \core_privacy\local\request\writer::with_context($context); + $portfoliodata = $writer->get_data([get_string('privacy:path', 'portfolio')]); + $this->assertEquals('Google Docs', $portfoliodata->{'Google Docs'}->name); + } + + /** + * Test that deleting only results in the one context being removed. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $this->create_portfolio_data('googledocs', 'Google Docs', $user1, 'visible', 1); + $this->create_portfolio_data('onedrive', 'Microsoft onedrive', $user2, 'visible', 1); + // Check a system context sent through. + $systemcontext = context_system::instance(); + \core_portfolio\privacy\provider::delete_data_for_all_users_in_context($systemcontext); + $records = $DB->get_records('portfolio_instance_user'); + $this->assertCount(2, $records); + $context = context_user::instance($user1->id); + \core_portfolio\privacy\provider::delete_data_for_all_users_in_context($context); + $records = $DB->get_records('portfolio_instance_user'); + // Only one entry should remain for user 2. + $this->assertCount(1, $records); + $data = array_shift($records); + $this->assertEquals($user2->id, $data->userid); + } + + /** + * Test that deleting only results in one user's data being removed. + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $this->create_portfolio_data('googledocs', 'Google Docs', $user1, 'visible', 1); + $this->create_portfolio_data('onedrive', 'Microsoft onedrive', $user2, 'visible', 1); + + $records = $DB->get_records('portfolio_instance_user'); + $this->assertCount(2, $records); + + $context = context_user::instance($user1->id); + $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_portfolio', [$context->id]); + \core_portfolio\privacy\provider::delete_data_for_user($contextlist); + $records = $DB->get_records('portfolio_instance_user'); + // Only one entry should remain for user 2. + $this->assertCount(1, $records); + $data = array_shift($records); + $this->assertEquals($user2->id, $data->userid); } }