diff --git a/blocks/html/classes/privacy/provider.php b/blocks/html/classes/privacy/provider.php new file mode 100644 index 00000000000..003562b59d4 --- /dev/null +++ b/blocks/html/classes/privacy/provider.php @@ -0,0 +1,183 @@ +. + +/** + * Privacy Subsystem implementation for block_html. + * + * @package block_html + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_html\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\helper; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; + +/** + * Privacy Subsystem implementation for block_html. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The block_html block stores user provided data. + \core_privacy\local\metadata\provider, + + // The block_html block provides data directly to core. + \core_privacy\local\request\plugin\provider { + + /** + * Returns information about how block_html stores its data. + * + * @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 { + $collection->link_subsystem('block', 'privacy:metadata:block'); + + 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) : \core_privacy\local\request\contextlist { + // This block doesn't know who information is stored against unless it + // is at the user context. + $contextlist = new \core_privacy\local\request\contextlist(); + + $sql = "SELECT c.id + FROM {block_instances} b + INNER JOIN {context} c ON c.instanceid = b.id AND c.contextlevel = :contextblock + INNER JOIN {context} bpc ON bpc.id = b.parentcontextid + WHERE b.blockname = 'html' + AND bpc.contextlevel = :contextuser + AND bpc.instanceid = :userid"; + + $params = [ + 'contextblock' => CONTEXT_BLOCK, + 'contextuser' => CONTEXT_USER, + 'userid' => $userid, + ]; + + $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; + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT + c.id AS contextid, + bi.* + FROM {context} c + INNER JOIN {block_instances} bi ON bi.id = c.instanceid AND c.contextlevel = :contextlevel + WHERE bi.blockname = 'html' + AND( + c.id {$contextsql} + ) + "; + + $params = [ + 'contextlevel' => CONTEXT_BLOCK, + ]; + $params += $contextparams; + + $instances = $DB->get_recordset_sql($sql, $params); + foreach ($instances as $instance) { + $context = \context_block::instance($instance->id); + $block = block_instance('html', $instance); + if (empty($block->config)) { + // Skip this block. It has not been configured. + continue; + } + + $html = writer::with_context($context) + ->rewrite_pluginfile_urls([], 'block_html', 'content', null, $block->config->text); + + // Default to FORMAT_HTML which is what will have been used before the + // editor was properly implemented for the block. + $format = isset($block->config->format) ? $block->config->format : FORMAT_HTML; + + $filteropt = (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + $html = format_text($html, $format, $filteropt); + + $data = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + $data->title = $block->config->title; + $data->content = $html; + + writer::with_context($context)->export_data([], $data); + } + $instances->close(); + } + + /** + * 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) { + // The only way to delete data for the html block is to delete the block instance itself. + blocks_delete_instance(static::get_instance_from_context($context)); + } + + /** + * 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) { + // The only way to delete data for the html block is to delete the block instance itself. + foreach ($contextlist as $context) { + blocks_delete_instance(static::get_instance_from_context($context)); + } + } + + /** + * Get the block instance record for the specified context. + * + * @param \context_block $context The context to fetch + * @return \stdClass + */ + protected static function get_instance_from_context(\context_block $context) { + global $DB; + + return $DB->get_record('block_instances', ['id' => $context->instanceid]); + } +} diff --git a/blocks/html/lang/en/block_html.php b/blocks/html/lang/en/block_html.php index c51692a42fe..66888161264 100644 --- a/blocks/html/lang/en/block_html.php +++ b/blocks/html/lang/en/block_html.php @@ -33,3 +33,4 @@ $string['html:myaddinstance'] = 'Add a new HTML block to Dashboard'; $string['newhtmlblock'] = '(new HTML block)'; $string['pluginname'] = 'HTML'; $string['search:content'] = 'HTML block content'; +$string['privacy:metadata:block'] = 'The HTML block stores all of its data within the block subsystem.'; diff --git a/blocks/html/tests/privacy_provider_test.php b/blocks/html/tests/privacy_provider_test.php new file mode 100644 index 00000000000..94d38ca1e58 --- /dev/null +++ b/blocks/html/tests/privacy_provider_test.php @@ -0,0 +1,344 @@ +. + +/** + * Unit tests for the block_html implementation of the privacy API. + * + * @package block_html + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \block_html\privacy\provider; + +/** + * Unit tests for the block_html implementation of the privacy API. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_html_privacy_testcase extends \core_privacy\tests\provider_testcase { + /** + * Get the list of standard format options for comparison. + * + * @return \stdClass + */ + protected function get_format_options() { + return (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + } + + /** + * Creates an HTML block on a user. + * + * @param string $title + * @param string $body + * @param string $format + * @return \block_instance + */ + protected function create_user_block($title, $body, $format) { + global $USER; + + $configdata = (object) [ + 'title' => $title, + 'text' => [ + 'itemid' => 19, + 'text' => $body, + 'format' => $format, + ], + ]; + + $this->create_block($this->construct_user_page($USER)); + $block = $this->get_last_block_on_page($this->construct_user_page($USER)); + $block = block_instance('html', $block->instance); + $block->instance_config_save((object) $configdata); + + return $block; + } + + /** + * Creates an HTML block on a course. + * + * @param \stdClass $course + * @param string $title + * @param string $body + * @param string $format + * @return \block_instance + */ + protected function create_course_block($course, $title, $body, $format) { + global $USER; + + $configdata = (object) [ + 'title' => $title, + 'text' => [ + 'itemid' => 19, + 'text' => $body, + 'format' => $format, + ], + ]; + + $this->create_block($this->construct_course_page($course)); + $block = $this->get_last_block_on_page($this->construct_course_page($course)); + $block = block_instance('html', $block->instance); + $block->instance_config_save((object) $configdata); + + return $block; + } + + /** + * Creates an HTML block on a page. + * + * @param \page $page Page + */ + protected function create_block($page) { + $page->blocks->add_block_at_end_of_default_region('html'); + } + + /** + * Get the last block on the page. + * + * @param \page $page Page + * @return \block_html Block instance object + */ + protected function get_last_block_on_page($page) { + $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region()); + $block = end($blocks); + + return $block; + } + + /** + * Constructs a Page object for the User Dashboard. + * + * @param \stdClass $user User to create Dashboard for. + * @return \moodle_page + */ + protected function construct_user_page(\stdClass $user) { + $page = new \moodle_page(); + $page->set_context(\context_user::instance($user->id)); + $page->set_pagelayout('mydashboard'); + $page->set_pagetype('my-index'); + $page->blocks->load_blocks(); + return $page; + } + + /** + * Constructs a Page object for the User Dashboard. + * + * @param \stdClass $course Course to create Dashboard for. + * @return \moodle_page + */ + protected function construct_course_page(\stdClass $course) { + $page = new \moodle_page(); + $page->set_context(\context_course::instance($course->id)); + $page->set_pagelayout('standard'); + $page->set_pagetype('course-view'); + $page->set_course($course); + $page->blocks->load_blocks(); + return $page; + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_user_block() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // Only the user context should be returned. + $this->assertCount(1, $contextlist); + $this->assertEquals($context, $contextlist->current()); + + // Export the data. + $this->export_context_data_for_user($user->id, $context, 'block_html'); + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + // Check the data. + $data = $writer->get_data([]); + $this->assertInstanceOf('stdClass', $data); + $this->assertEquals($title, $data->title); + $this->assertEquals(format_text($content, $format, $this->get_format_options()), $data->content); + + // Delete the context. + provider::delete_data_for_all_users_in_context($context); + + // Re-fetch the contexts - it should no longer be returned. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard which is not configured is _not_ exported. + */ + public function test_user_block_unconfigured() { + global $DB; + + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $block = $this->create_user_block($title, $content, $format); + $block->instance->configdata = ''; + $DB->update_record('block_instances', $block->instance); + $block = block_instance('html', $block->instance); + + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // Only the user context should be returned. + $this->assertCount(1, $contextlist); + $this->assertEquals($context, $contextlist->current()); + + // Export the data. + $this->export_context_data_for_user($user->id, $context, 'block_html'); + $writer = \core_privacy\local\request\writer::with_context($context); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_user_multiple_blocks_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $blocks = []; + $contexts = []; + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // There are now two blocks on the user context. + $this->assertCount(2, $contextlist); + foreach ($contextlist as $context) { + $this->assertTrue(isset($contexts[$context->id])); + } + + // Turn them into an approved_contextlist. + $approvedlist = new approved_contextlist($user, 'block_html', $contextlist->get_contextids()); + + // Delete using delete_data_for_user. + provider::delete_data_for_user($approvedlist); + + // Re-fetch the contexts - it should no longer be returned. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard is not exported. + */ + public function test_course_blocks_not_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $this->setUser($user); + + $block = $this->create_course_block($course, $title, $content, $format); + $context = \context_block::instance($block->instance->id); + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // No blocks should be returned. + $this->assertCount(0, $contextlist); + } + + /** + * Test that a block on the dashboard is exported. + */ + public function test_mixed_multiple_blocks_exported() { + $this->resetAfterTest(); + + $title = 'Example title'; + $content = 'Example content'; + $format = FORMAT_PLAIN; + + // Test setup. + $contexts = []; + + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $this->setUser($user); + + $block = $this->create_course_block($course, $title, $content, $format); + $context = \context_block::instance($block->instance->id); + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + $block = $this->create_user_block($title, $content, $format); + $context = \context_block::instance($block->instance->id); + $contexts[$context->id] = $context; + + // Get the contexts. + $contextlist = provider::get_contexts_for_userid($user->id); + + // There are now two blocks on the user context. + $this->assertCount(2, $contextlist); + foreach ($contextlist as $context) { + $this->assertTrue(isset($contexts[$context->id])); + } + } +}