diff --git a/lang/en/portfolio.php b/lang/en/portfolio.php index 792cb6ec7d6..32d846aa813 100644 --- a/lang/en/portfolio.php +++ b/lang/en/portfolio.php @@ -170,6 +170,16 @@ $string['privacy:metadata'] = 'The portfolio subsystem acts as a channel, passin $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:portfolio_log'] = 'Log of portfolio transfers (used to later check for duplicates)'; +$string['privacy:metadata:portfolio_log:caller_class'] = 'Name of the class used to create the transfer'; +$string['privacy:metadata:portfolio_log:caller_component'] = 'Component name responsible for exporting'; +$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started)'; +$string['privacy:metadata:portfolio_log:userid'] = 'User who exported content'; +$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports, cleaned by cron after one day'; +$string['privacy:metadata:portfolio_tempdata:data'] = 'Export data'; +$string['privacy:metadata:portfolio_tempdata:expirytime'] = 'Time this record will expire'; +$string['privacy:metadata:portfolio_tempdata:instance'] = 'Portfolio plugin instance being used'; +$string['privacy:metadata:portfolio_tempdata:userid'] = 'User performing export'; $string['privacy:metadata:value'] = 'Value for the preference'; $string['privacy:metadata:userid'] = 'The user Identifier.'; $string['privacy:path'] = 'Portfolio instances'; diff --git a/portfolio/classes/privacy/provider.php b/portfolio/classes/privacy/provider.php index 3f1b7951a4a..8ea61c9f3f1 100644 --- a/portfolio/classes/privacy/provider.php +++ b/portfolio/classes/privacy/provider.php @@ -29,6 +29,7 @@ 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; +use core_privacy\local\request\transform; /** * Provider for the portfolio API. @@ -56,6 +57,22 @@ class provider implements 'name' => 'privacy:metadata:name', 'value' => 'privacy:metadata:value' ], 'privacy:metadata:instancesummary'); + + $collection->add_database_table('portfolio_log', [ + 'userid' => 'privacy:metadata:portfolio_log:userid', + 'time' => 'privacy:metadata:portfolio_log:time', + 'caller_class' => 'privacy:metadata:portfolio_log:caller_class', + 'caller_component' => 'privacy:metadata:portfolio_log:caller_component', + ], 'privacy:metadata:portfolio_log'); + + // Temporary data is not exported/deleted in privacy API. It is cleaned by cron. + $collection->add_database_table('portfolio_tempdata', [ + 'data' => 'privacy:metadata:portfolio_tempdata:data', + 'expirytime' => 'privacy:metadata:portfolio_tempdata:expirytime', + 'userid' => 'privacy:metadata:portfolio_tempdata:userid', + 'instance' => 'privacy:metadata:portfolio_tempdata:instance', + ], 'privacy:metadata:portfolio_tempdata'); + $collection->add_plugintype_link('portfolio', [], 'privacy:metadata'); return $collection; } @@ -69,9 +86,11 @@ class provider implements 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]; + WHERE ctx.instanceid = :userid AND ctx.contextlevel = :usercontext + AND (EXISTS (SELECT 1 FROM {portfolio_instance_user} WHERE userid = :userid1) OR + EXISTS (SELECT 1 FROM {portfolio_log} WHERE userid = :userid2)) + "; + $params = ['userid' => $userid, 'usercontext' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; @@ -95,16 +114,63 @@ class provider implements } }); + if (empty($correctusercontext)) { + return; + } + $usercontext = array_shift($correctusercontext); + $sql = "SELECT pi.id AS instanceid, pi.name, + piu.id AS preferenceid, piu.name AS preference, piu.value, + pl.id AS logid, pl.time AS logtime, pl.caller_class, pl.caller_file, + pl.caller_component, pl.returnurl, pl.continueurl + FROM {portfolio_instance} pi + LEFT JOIN {portfolio_instance_user} piu ON piu.instance = pi.id AND piu.userid = :userid1 + LEFT JOIN {portfolio_log} pl ON pl.portfolio = pi.id AND pl.userid = :userid2 + WHERE piu.id IS NOT NULL OR pl.id IS NOT NULL"; + $params = ['userid1' => $usercontext->instanceid, 'userid2' => $usercontext->instanceid]; + $instances = []; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $record) { + $instances += [$record->name => + (object)[ + 'name' => $record->name, + 'preferences' => [], + 'logs' => [], + ] + ]; + if ($record->preferenceid) { + $instances[$record->name]->preferences[$record->preferenceid] = (object)[ + 'name' => $record->preference, + 'value' => $record->value, + ]; + } + if ($record->logid) { + $instances[$record->name]->logs[$record->logid] = (object)[ + 'time' => transform::datetime($record->logtime), + 'caller_class' => $record->caller_class, + 'caller_file' => $record->caller_file, + 'caller_component' => $record->caller_component, + 'returnurl' => $record->returnurl, + 'continueurl' => $record->continueurl + ]; + } + } + $rs->close(); - $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)) { + foreach ($instances as &$instance) { + if (!empty($instance->preferences)) { + $instance->preferences = array_values($instance->preferences); + } else { + unset($instance->preferences); + } + if (!empty($instance->logs)) { + $instance->logs = array_values($instance->logs); + } else { + unset($instance->logs); + } + } \core_privacy\local\request\writer::with_context($contextlist->current())->export_data( [get_string('privacy:path', 'portfolio')], (object) $instances); } @@ -120,6 +186,8 @@ class provider implements // Context could be anything, BEWARE! if ($context->contextlevel == CONTEXT_USER) { $DB->delete_records('portfolio_instance_user', ['userid' => $context->instanceid]); + $DB->delete_records('portfolio_tempdata', ['userid' => $context->instanceid]); + $DB->delete_records('portfolio_log', ['userid' => $context->instanceid]); } } @@ -141,9 +209,15 @@ class provider implements } }); + if (empty($correctusercontext)) { + return; + } + $usercontext = array_shift($correctusercontext); $DB->delete_records('portfolio_instance_user', ['userid' => $usercontext->instanceid]); + $DB->delete_records('portfolio_tempdata', ['userid' => $usercontext->instanceid]); + $DB->delete_records('portfolio_log', ['userid' => $usercontext->instanceid]); } /** diff --git a/portfolio/tests/privacy_provider_test.php b/portfolio/tests/privacy_provider_test.php index 93ffb98167c..a1e2a663969 100644 --- a/portfolio/tests/privacy_provider_test.php +++ b/portfolio/tests/privacy_provider_test.php @@ -47,6 +47,22 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc 'value' => $value ]; $DB->insert_record('portfolio_instance_user', $userinstance); + + $DB->insert_record('portfolio_log', [ + 'portfolio' => $portfolioinstance->id, + 'userid' => $user->id, + 'caller_class' => 'forum_portfolio_caller', + 'caller_component' => 'mod_forum', + 'time' => time(), + ]); + + $DB->insert_record('portfolio_log', [ + 'portfolio' => $portfolioinstance->id, + 'userid' => $user->id, + 'caller_class' => 'workshop_portfolio_caller', + 'caller_component' => 'mod_workshop', + 'time' => time(), + ]); } /** @@ -57,9 +73,11 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $collection = \core_portfolio\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); $items = $collection->get_collection(); - $this->assertEquals(2, count($items)); + $this->assertEquals(4, 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]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[1]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[2]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\plugintype_link::class, $items[3]); } /** @@ -105,6 +123,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc \core_portfolio\privacy\provider::delete_data_for_all_users_in_context($systemcontext); $records = $DB->get_records('portfolio_instance_user'); $this->assertCount(2, $records); + $this->assertCount(4, $DB->get_records('portfolio_log')); $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'); @@ -112,6 +131,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $this->assertCount(1, $records); $data = array_shift($records); $this->assertEquals($user2->id, $data->userid); + $this->assertCount(2, $DB->get_records('portfolio_log')); } /** @@ -128,6 +148,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $records = $DB->get_records('portfolio_instance_user'); $this->assertCount(2, $records); + $this->assertCount(4, $DB->get_records('portfolio_log')); $context = context_user::instance($user1->id); $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_portfolio', [$context->id]); @@ -137,5 +158,6 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $this->assertCount(1, $records); $data = array_shift($records); $this->assertEquals($user2->id, $data->userid); + $this->assertCount(2, $DB->get_records('portfolio_log')); } }